Skip to content
Commitpedia
Log in Join & edit

Write a Dockerfile: FROM, COPY, RUN, CMD and multi-stage builds

How a Dockerfile turns into an image layer by layer, what the main instructions do, how to order them for fast rebuilds, and how a multi-stage build keeps the final image small.

Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?

Edit history of Write a Dockerfile: FROM, COPY, RUN, CMD and multi-stage builds: 1 recorded edit
Edit history · 1 edit
Type
Article
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Reading
3 min

A Dockerfile is a plain text recipe. docker build reads it top to bottom and runs each instruction in a temporary container, saving the result as a new image layer. Understanding that each line is a layer explains both why builds can be fast (unchanged layers are reused from cache) and why images can get bloated (files deleted in a later layer still exist in an earlier one).

A first Dockerfile

FROM python:3.13-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["python", "app.py"]
  • FROM picks the base image. Always use a specific tag such as 3.13-slim; latest changes under you.
  • WORKDIR sets the directory for the following instructions and creates it if needed.
  • COPY src dest copies files from the build context (the directory you pass to docker build) into the image. ADD does the same plus URL downloads and tar extraction; prefer COPY unless you need those.
  • RUN executes a command at build time and saves the result as a layer. Combine related commands with && so they share one layer.
  • EXPOSE documents the port the program listens on. It does not publish anything; -p at run time does.
  • CMD is the default command when a container starts. The JSON array form avoids a shell wrapper and lets signals such as Ctrl+C reach the process.

Build and run it:

docker build -t myapp:1.0 .
docker run --rm -p 8000:8000 myapp:1.0

Order instructions for the cache

Docker reuses a cached layer only if the instruction and all layers before it are unchanged. For COPY, that means the copied files must be identical. The example above copies requirements.txt and installs dependencies before copying the rest of the source. Editing app.py then only invalidates the final COPY, and the slow pip install layer comes from cache. Put the things that change least at the top.

Keep files out with .dockerignore

Everything in the build context is sent to the daemon and is available to COPY . .. A .dockerignore file next to the Dockerfile excludes what should never end up in the image:

.git
.venv
__pycache__
node_modules
*.log
.env

CMD vs ENTRYPOINT

CMD is replaced entirely if you pass a command to docker run, for example docker run myapp:1.0 python -m pytest. ENTRYPOINT is always run and anything you pass becomes its arguments. The two combine: use ENTRYPOINT for the fixed program and CMD for default arguments.

ENTRYPOINT ["python", "manage.py"]
CMD ["runserver", "0.0.0.0:8000"]

Do not run as root

By default the process inside a container runs as root. Create a user in the image and switch to it after the installation steps that need root:

RUN useradd --create-home --shell /bin/bash appuser
USER appuser

Multi-stage builds

Compiled languages need a compiler and build tools, but the finished program does not. A multi-stage build uses one FROM to build and a second, smaller FROM for the final image, copying only the result across. The build tools never appear in the image you ship.

FROM golang:1.27 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server .

FROM gcr.io/distroless/static-debian12
COPY --from=build /out/server /server
USER nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]

The same pattern works for Node.js front ends (build with node, serve with nginx) and for Java (build with a JDK image, run on a JRE image). docker build --target build -t myapp:build . stops after the first stage when you want to debug it.

Checking the result

docker image ls myapp
docker history myapp:1.0
docker run --rm myapp:1.0 id

history shows each layer with its size, which is the quickest way to spot an instruction that made the image unexpectedly large.

Official documentation

Did this work for you?

No votes yet. Be the first.

If it did not work, the report form below tells the editors what went wrong.

See also

  • 0055 Docker basics: images, containers and the commands you need An image is a read-only package of an app and its dependencies, and a container is a running instance of it. Learn… Beginner · Explains an idea · Linux, macOS, Windows Article Docker basics 15 Oct 2026
  • 0056 Docker Compose: run a small web app with a database Docker Compose describes several containers, their ports, volumes and environment in one compose.yaml file, then starts them all with docker compose up. Intermediate · Step by step · Linux, macOS, Windows Article Docker basics · Docker Compose 15 Oct 2026
  • 0250 Containerize a Node.js app and run it with PostgreSQL using Docker Compose Package a small Node.js web app in a multi-stage Docker image, then run it next to PostgreSQL with Docker Compose, named volumes,… Intermediate · Guided project · Linux, macOS, Windows Article Docker basics · Docker Compose · PostgreSQL 15 Oct 2026
  • 0268 What is a container? A container is a lightweight box that packs a program with everything it needs so it runs the same on any computer. Beginner · Explains an idea · Any system Article Docker basics 15 Oct 2026
  • 0327 Docker images, containers and volumes explained An image is a read-only template, a container is a running copy of it with a thin writable layer, and a volume… Beginner · Explains an idea · Linux, macOS, Windows Article Docker basics 15 Oct 2026
  • 0328 Install Docker Engine on Ubuntu and Debian Set up Docker's official apt repository, install Docker Engine with the Compose and Buildx plugins, run hello-world and let your user run… Beginner · Step by step · Linux Article Docker basics · Packages & updates 15 Oct 2026

Repos in the wild

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Dominik +3,932 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?