What is a Dockerfile? #

The Dockerfile is one of the simplest files in the Docker ecosystem, and also one of the most influential. It’s just a plain text file, with no special extension, no binary, no secret format. But from this small file come the images that are the foundation of containers, the backbone of CI/CD pipelines, and the contract between developers and production. Almost everything you do with Docker starts here.

The question that often comes up early in the Docker learning journey usually isn’t “what is a Dockerfile?” — the answer is easy to find. The more important question is: why can such a simple file be so crucial, and what concepts behind it must be understood before writing the first line? Without that understanding, a Dockerfile feels like a sequence of magic instructions to memorize. With it, a Dockerfile becomes a tool you can design, optimize, and debug to fit your needs.

This article is the gateway to everything discussed in the Dockerfile section. After reading it, you’ll understand what a Dockerfile is, its place in the Docker workflow, the most commonly used instructions, and the mindset that separates a carelessly written Dockerfile from a production-ready one.

The Problem the Dockerfile Solves #

To appreciate the Dockerfile, you need to see the problem it was built to solve. That problem has a name: “works on my machine” — the legendary syndrome of software engineering.

In the traditional approach, applications run directly on servers. Libraries are installed globally, runtime versions are decided by admins, and configuration is scattered across many places. The result: an application that runs perfectly on a developer’s laptop often fails on staging, and fails even more often in production. Every environment change becomes a gamble: are the dependencies the same? Are the versions compatible? Is the configuration consistent?

The Dockerfile answers this by declaring the application environment as code. Everything needed to run the application — base OS, runtime, libraries, configuration, and start command — is written in one file. That file can be committed to Git, reviewed, tested, and run anywhere consistently.

flowchart LR
    A[Traditional Application] --> B[Server: OS + Global Libs]
    C[Application + Dockerfile] --> D[Image: App + Deps + Runtime]
    B --> E[Dependency conflicts]
    D --> F[Consistent anywhere]

With a Dockerfile, the phrase “works on my machine” loses its meaning. There’s only one question left: “is the Dockerfile correct?” If it is, everyone — from developer laptops to Kubernetes clusters — will run the exact same application.


Defining the Dockerfile #

A Dockerfile is a text file containing sequential instructions for building a Docker image. Each instruction line is a command the Docker daemon executes during docker build. The result is an image — a read-only blueprint ready to be run as a container.

Conceptually, a Dockerfile is the recipe for making an image. If a container is the dish served at the table, an image is the prepared, packaged meal, and the Dockerfile is the cookbook explaining the ingredients, order, and cooking techniques.

What’s declared in a Dockerfile:

  • Base image — the system foundation to use (e.g. node:20-alpine, python:3.12-slim).
  • Dependencies — libraries, packages, and tools the application needs.
  • Application files — code, configuration, and assets to copy into the image.
  • Environment variables — values available when the container runs.
  • Startup commands — what to run when the container starts.

All this information used to be scattered across many places: team wikis, READMEs, manual install scripts, and developers’ collective memory. The Dockerfile unifies it all into one file that can be read, tested, and audited.


The Dockerfile’s Place in the Docker Workflow #

The Dockerfile is the starting point of the entire Docker workflow. It isn’t an image or a container — it’s just a recipe. But from this recipe, Docker produces two different things.

flowchart LR
    A[Dockerfile] -->|docker build| B[Docker Image]
    B -->|docker run| C[Docker Container]
    B -->|docker push| D[Registry]
    D -->|docker pull| C
  • Dockerfile — a text file with build instructions. This is what you write and keep in the repository.
  • Docker Image — the build result from the Dockerfile. Images are read-only, immutable, and distributable.
  • Docker Container — the runtime instance of an image. A container is a running process with mutable state.

What you should underline: the Dockerfile is never executed directly by the container. Containers run from images, and images are built from Dockerfiles. The Dockerfile is always one step behind.

Commands relevant to each stage:

# Build an image from a Dockerfile
docker build -t myapp:1.0 .

# Run an image as a container
docker run -d -p 8080:80 myapp:1.0

# Distribute the image to a registry
docker push myapp:1.0

docker build reads the Dockerfile from the first line to the last, executes each instruction, and assembles the image layer by layer. docker run never sees the Dockerfile’s contents — it only sees the final image.


Dockerfile Anatomy: A First Example #

To see what a Dockerfile looks like in its simplest form, consider the example below.

# Comment lines are ignored by Docker
FROM node:20-alpine

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm install

COPY . .

EXPOSE 3000
CMD ["npm", "start"]

Each line has a role:

  • FROM — sets the base image. This is mandatory and must be the first instruction (unless a special ARG precedes FROM).
  • WORKDIR — sets the working directory inside the image. After this line, all relative paths are based on that directory.
  • COPY — copies files from the host (build context) into the image.
  • RUN — executes commands during the build. Here, npm install runs to install dependencies.
  • EXPOSE — documents the port the application will use. Documentation only, doesn’t open the port.
  • CMD — sets the default command when the container runs. Only one CMD allowed (the last one overrides the previous).

The order of these lines isn’t accidental. package.json and package-lock.json are copied and installed before the entire source code is copied. This is deliberate, to leverage Docker’s layer cache — a topic covered in depth later, but the gist: rarely-changing instructions go on top, frequently-changing ones at the bottom.

// ANTI-PATTERN: COPY all code up front; the npm install cache is always invalidated
FROM node:20
WORKDIR /app
COPY . .
RUN npm install

// CORRECT: COPY dependencies first; npm install can be cached between builds
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .

With the second pattern, every time you change application code, Docker doesn’t have to re-run npm install — the npm install layer is reused from cache. Builds become far faster.


The Infrastructure-as-Code Philosophy #

A Dockerfile isn’t just configuration — it’s code. That means it’s subject to software engineering principles: version control, code review, testing, and reproducibility.

Version control. The Dockerfile lives in Git alongside the application code. Every change is recorded, every diff can be reviewed, and every version can be reverted.

Code review. Pull requests for Dockerfiles are as important as pull requests for application code. A bad image deployed to production can become a security, stability, and cost problem.

Testing. Dockerfiles can be tested. Tools like hadolint validate Dockerfiles against best practices. CI/CD pipelines can ensure built images pass vulnerability scans before being pushed.

Reproducibility. The same Dockerfile, with the same context, produces the same image. No more “but it’s different on my server”.

flowchart TD
    A[Code + Dockerfile in Git] --> B[CI/CD Pipeline]
    B --> C[docker build]
    C --> D{Valid result?}
    D -- Yes --> E[docker push to Registry]
    D -- No --> F[Failed: notify developers]
    E --> G[Deploy to Staging]
    G --> H{Staging OK?}
    H -- Yes --> I[Deploy to Production]
    H -- No --> F

This philosophy differs from the traditional approach, where environment setup is the server admin’s responsibility, often undocumented, and changes over time. With a Dockerfile, the application environment is defined, deterministic, and auditable.


The Dockerfile’s Role in Modern Pipelines #

The Dockerfile is the contract connecting five parties: developers, CI/CD, the registry, the orchestrator, and the production environment. Each party interacts with the Dockerfile’s output, not the Dockerfile itself.

Developers write and maintain the Dockerfile. Their goal: produce an image representative of the application.

CI/CD executes docker build on every code change. Their goal: produce a new, testable image.

The registry stores the finished images. Its goal: be the single source of truth for images to be deployed.

The orchestrator (Kubernetes, ECS, Nomad) pulls images from the registry and runs them. Its goal: provide a runtime platform for containers.

Production is where images actually run serving users. Its goal: stability, performance, and reliability.

Because these five parties communicate through images, Dockerfile quality determines the quality of the entire pipeline. A bad Dockerfile = a bad image = inconsistent deploys = preventable incidents.


Dockerfile vs Docker Compose: Don’t Mix Them Up #

One of the most common early-confusions in the Docker journey is mixing up the Dockerfile with Docker Compose. Both use YAML-like formats, and both often appear together. But their roles are completely different.

AspectDockerfileDocker Compose
PurposeBuild an imageRun many containers
FocusOne applicationMulti-service architecture
FormatImperative instructionsDeclarative (YAML)
Commanddocker builddocker compose up
OutputImageRunning containers
Used atBuild timeRuntime

A Dockerfile answers the question: “How do I create an image for this application?” Docker Compose answers: “This application needs a database, cache, and message broker — how do I run them all at once?”

# docker-compose.yml — not a Dockerfile
services:
  app:
    build: .
    ports:
      - "3000:3000"
    depends_on:
      - db
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret

In the example above, build: . tells Docker Compose to read the Dockerfile from the current directory. But once the image is built, Compose’s role is orchestrating containers, not building images.

Dockerfiles are mandatory for custom images. Docker Compose is optional — you can run multi-container setups without Compose, but it won’t be as comfortable or ergonomic. For local development, Docker Compose is almost always the primary choice.


The Layer and Cache Concept #

One of the most important Dockerfile concepts is the layer. Each Dockerfile instruction produces one layer in the image. These layers are cached and reused in subsequent builds. Understanding layers means understanding why instruction order determines build speed.

flowchart TB
    subgraph Image["Docker Image (read-only)"]
        L1[Layer 1: Base OS - FROM]
        L2[Layer 2: WORKDIR]
        L3[Layer 3: COPY package.json]
        L4[Layer 4: RUN npm install]
        L5[Layer 5: COPY source code]
    end
    L1 --> L2 --> L3 --> L4 --> L5

Docker compares new layers against the cache from previous builds. If a layer is identical, Docker reuses the cache. If not, Docker rebuilds that layer and every layer after it.

The implications are very concrete:

  • If package.json doesn’t change, the npm install layer is reused — fast builds.
  • If package.json changes, the npm install layer is rebuilt — and every layer after it (including COPY source code) is rebuilt too.

That’s why frequently-changing instructions go at the bottom, and rarely-changing instructions at the top. This principle is covered in depth in the best-practice article, but for now, just remember: Dockerfile order isn’t about aesthetics, it’s about build performance.

// ✗ Anti-pattern: source code on top, cache often invalidated
FROM node:20
WORKDIR /app
COPY . .
RUN npm install

// ✓ Correct: dependencies first, source code last
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .

Common Mistakes When Writing Dockerfiles #

Dockerfiles look simple, and they are simple — on the surface. But there are several traps that often snare beginners (and even experienced developers). Recognizing these traps early will save a lot of time.

1. Base images too large. Choosing ubuntu:latest or node:latest for production bloats the image and widens the attack surface. Always start from the base image that best fits your needs.

2. Not leveraging the layer cache. Copying all the code up front, then running npm install, makes the cache never useful. Instruction order is everything.

3. Copying sensitive files without .dockerignore. .env files, .git, local node_modules folders, and credentials can end up in the build context. Without .dockerignore, you send more data to the daemon and potentially leak secrets.

4. Running applications as root. The default is for containers to run as root. That’s dangerous — if a container is exploited, the attacker gets root access on the host. Always create a non-root user.

5. Combining too many RUN commands without cleanup. Installing packages without removing the cache in the same layer bloats it. apt, apk, and pip caches must be removed in the same RUN as the installation.

A bad Dockerfile = slow builds + large images + security risk. Fortunately, all these traps can be avoided with discipline and the right mindset.


When Do You Need a Dockerfile? #

A Dockerfile is mandatory when you want an image that’s representative of your application. Without a Dockerfile, you can only use public images from Docker Hub — which rarely fit your application’s specific needs.

A Dockerfile fits well when:

  • The application will be deployed to any environment (cloud, on-premise, Kubernetes, serverless).
  • You use CI/CD and want automatic image builds from code.
  • Your team has many people with different local environments.
  • The application is a microservice with one main process per service.
  • You need reproducibility — the same image must be buildable anytime, anywhere.

A Dockerfile is also very helpful even for local experiments. You don’t need to start with Kubernetes to benefit from a Dockerfile. Just have a good Dockerfile, and your application is ready to move to any environment without surprises.


Summary #

  • A Dockerfile is a text file with sequential instructions for building a Docker image. It’s the recipe, not the dish — not an image, not a container.
  • The Dockerfile’s place is at the start of the flow: Dockerfile → Image → Container. It’s processed by docker build, not run directly.
  • Basic Dockerfile anatomy: FROM (base image), WORKDIR (working directory), COPY/ADD (copy files), RUN (execute commands at build), CMD/ENTRYPOINT (commands when the container runs).
  • The main Dockerfile philosophy is infrastructure as code: environment configuration is written, reviewed, and version-controlled like application code.
  • Layers and cache are fundamental concepts. Instruction order determines whether the next build can reuse cache or must rebuild from scratch.
  • Dockerfile ≠ Docker Compose. Dockerfiles build images for one application. Docker Compose runs many containers together.
  • Common mistakes to avoid: oversized base images, wrong instruction order, no .dockerignore, root users, and forgetting cache cleanup.
  • When you need a Dockerfile — almost always, as long as you want a representative, portable image for your application.

← Previous: User-defined Network   Next: Dockerfile Structure →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact