Dockerfile Basics
This page covers the core Dockerfile instructions you use in almost every build. Each example is small and self-contained so you can copy, run, and adapt it.
Prerequisites
- Docker Engine 29.x with BuildKit (the default backend).
- Compose v2 available as
docker composeif you build via Compose. - A terminal and a project directory to act as the build context.
docker version
# confirm Engine 29.x; BuildKit is on by defaultBasic Examples
1. FROM - choose a base image
FROM sets the parent image every later instruction builds on.
FROM node:22-slim- Must be the first instruction (after optional
ARG). - Prefer a specific tag over
latestso builds are predictable. - Slim and Alpine variants are smaller but carry fewer libraries.
- Pin to a digest (
node:22-slim@sha256:...) for reproducible builds.
2. WORKDIR - set the working directory
WORKDIR sets the directory for later RUN, CMD, COPY, and ENTRYPOINT.
WORKDIR /app- Creates the directory if it does not exist.
- Use it instead of
RUN cd, which does not persist across instructions. - Relative paths in later instructions resolve against it.
3. COPY - add files from the build context
COPY moves files from your context into the image.
COPY package.json package-lock.json ./- Source paths are relative to the build context root.
- Copy dependency manifests before source to protect the cache.
- Prefer
COPYoverADDunless you needADD's extra behavior.
4. RUN - execute build-time commands
RUN runs a command and commits the result as a new layer.
RUN npm ci --omit=dev- Each
RUNcreates one layer, so chain related commands with&&. - Clean up caches in the same
RUNso junk never enters a layer. - The exec form (
RUN ["executable", "arg"]) skips the shell.
5. ENV - set environment variables
ENV sets variables baked into the image and visible at runtime.
ENV NODE_ENV=production- Values persist into running containers.
- Do not store secrets here; they end up in the image config.
- Changing an
ENVinvalidates the cache for later steps.
6. EXPOSE - document the listening port
EXPOSE records which port the container process listens on.
EXPOSE 3000- It is documentation and metadata, not a firewall or publish rule.
- You still map ports at run time with
-por in Kubernetes. - Helps readers and tooling understand the contract.
7. CMD - default command
CMD sets the default process the container runs.
CMD ["node", "server.js"]- Use the exec (JSON array) form so signals reach the process directly.
- The shell form (
CMD node server.js) wraps the process in/bin/sh -c. - Only the last
CMDin a file takes effect. docker run myapp argoverridesCMDentirely.
Intermediate Examples
8. ENTRYPOINT plus CMD - fixed command with default args
ENTRYPOINT fixes the executable; CMD supplies overridable default arguments.
ENTRYPOINT ["python", "-m", "myservice"]
CMD ["--port=8080"]- Runtime args append to
ENTRYPOINT, replacingCMD. docker run img --port=9090runspython -m myservice --port=9090.- Use the exec form for both so the process is PID 1 and receives signals.
- Reserve this pattern for images that wrap one fixed binary.
9. USER - drop root
USER switches the identity that later instructions and the container run as.
RUN adduser --system --uid 10001 appuser
USER 10001
CMD ["node", "server.js"]- Set
USERafter installing packages, which usually needs root. - Use a numeric UID so Kubernetes
runAsNonRootcan verify it. - Ensure files the app writes are owned by that UID.
10. ARG plus multi-stage - build-time variables
ARG declares a build-time variable; a builder stage keeps tools out of the final image.
ARG GO_VERSION=1.23
FROM golang:${GO_VERSION} AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server
FROM gcr.io/distroless/static:nonroot
COPY --from=build /app /app
USER nonroot
ENTRYPOINT ["/app"]ARGvalues exist only during build, unlikeENV.- Pass overrides with
docker build --build-arg GO_VERSION=1.24 .. COPY --from=buildpulls only the compiled binary forward.- The final image ships no compiler or shell.
11. RUN with chained cleanup - keep layers small
Chain related commands in one RUN and clean caches in the same layer so nothing extra ships.
FROM debian:12-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*- Each
RUNis one layer, so chaining avoids a separate layer per command. - Deleting the apt lists in the same
RUNkeeps that junk out of the layer. --no-install-recommendsskips optional packages you rarely need.- A cleanup done in a later
RUNwould not shrink the earlier layer.
12. HEALTHCHECK - report container health
HEALTHCHECK tells Docker how to test whether the process is actually serving.
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -fsS http://localhost:3000/healthz || exit 1- Docker marks the container
healthyorunhealthyfrom the exit code. - Kubernetes ignores
HEALTHCHECKand uses its own liveness and readiness probes instead. - Keep the check cheap; it runs on every interval.
- Use it mainly for Docker and Compose environments.
13. LABEL - attach image metadata
LABEL records key-value metadata on the image, such as source and version.
LABEL org.opencontainers.image.source="https://github.com/acme/api" \
org.opencontainers.image.version="1.0"- Use the OCI
org.opencontainers.image.*keys so registries and tools recognize them. - Labels are metadata only and add no filesystem layer.
- Read them back with
docker inspect. - They help trace an image to its source commit and build.
- CI systems can populate the version and revision labels automatically at build time.
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).