Skip to content
docktionary

Docker

Dockerfile reference

Every instruction a Dockerfile can use, what it does, and a minimal example. Read top to bottom once, then come back to this page as a lookup.

FROM

Sets the base image for the stage that follows. Every Dockerfile starts with at least one. A Dockerfile can have several FROM lines — see multi-stage builds below.

dockerfile
FROM node:22-alpine

WORKDIR

Sets the working directory for every instruction after it, and creates the directory if it doesn't exist. Prefer this over RUN cd ..., which only affects that one instruction.

dockerfile
WORKDIR /app

COPY

Copies files from the build context (the directory you ran docker build from) into the image.

dockerfile
COPY package.json package-lock.json ./
COPY . .
COPY --chown=node:node . .

ADD

Like COPY, but can also fetch a remote URL and auto-extract local tar archives. Prefer COPY unless you specifically need one of those two extra behaviors — it's more predictable.

dockerfile
ADD app.tar.gz /opt/app/

RUN

Executes a command in a new layer at build time. Chain related commands with && so they form a single layer and clean up in the same step.

dockerfile
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*

CMD

The default command for the container, run if docker run isn't given one. Only the last CMD in a Dockerfile takes effect. Use the exec form (JSON array) so signals like SIGTERM reach your process directly instead of a shell.

dockerfile
CMD ["node", "server.js"]

ENTRYPOINT

The command that always runs. When both are set, CMD supplies default arguments to ENTRYPOINT — arguments a user can override on the command line, while the entrypoint itself generally can't be.

dockerfile
ENTRYPOINT ["node"]
CMD ["server.js"]
# docker run myapp worker.js  -> runs "node worker.js"

ENV

Sets an environment variable, available at build time to later instructions and at run time inside the container.

dockerfile
ENV NODE_ENV=production
ENV PORT=3000

ARG

Declares a build-time variable, passed with docker build --build-arg. Unlike ENV, it is not available in the running container unless you also assign it to an ENV.

dockerfile
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-alpine

EXPOSE

Documents which port the container listens on. It's metadata only — it does not publish the port. You still need -p on docker run to actually reach it from the host.

dockerfile
EXPOSE 3000

VOLUME

Marks a path as holding data that should live outside the container's writable layer. Docker creates an anonymous volume there if nothing else is mounted to that path at run time.

dockerfile
VOLUME /var/lib/postgresql/data

USER

Switches the user for the rest of the build and for the running container. Running as root inside a container is the default but not the safe choice — set a non-root user for anything facing the network.

dockerfile
RUN addgroup -S app && adduser -S app -G app
USER app

LABEL

Attaches arbitrary metadata to the image as key-value pairs — maintainer, source repo, version — queryable with docker inspect.

dockerfile
LABEL org.opencontainers.image.source="https://github.com/you/app"

HEALTHCHECK

Tells Docker how to actively check that the container is working, not just running. Surfaced in docker ps as healthy/unhealthy.

dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1

ONBUILD

Registers an instruction to run later, when this image is used as the base for another FROM. Mostly seen in base images meant to be extended — rare in application Dockerfiles.

dockerfile
ONBUILD COPY . /app

STOPSIGNAL

Sets which signal docker stop sends to the main process. Defaults to SIGTERM; change it if your process shuts down more cleanly on a different signal.

dockerfile
STOPSIGNAL SIGINT

SHELL

Changes which shell the shell-form of RUN, CMD, and ENTRYPOINT use. Useful on Windows containers, or to switch to bash for a script that needs it.

dockerfile
SHELL ["/bin/bash", "-c"]

Multi-stage builds

Use more than one FROM to keep build-only tools out of the final image. Name a stage with AS, then pull just the finished artifact out of it with COPY --from. Only the last stage ships — everything before it (compilers, source maps, test dependencies, your entire node_modules dev tree) exists only during the build and is discarded. For why this matters and how stages work under the hood, see multi-stage builds.

Dockerfile
# --- build stage: has the full toolchain, never shipped ---
FROM node:22 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# --- runtime stage: just the output, on a minimal base ---
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

Referencing a stage by number instead of name

Naming a stage with AS is optional. Every stage can also be referenced by its position, counting from zero — handy for a quick one-off stage you don't want to bother naming.

dockerfile
FROM node:22 AS build
# ...
FROM node:22-alpine
COPY --from=0 /app/dist ./dist   # same as --from=build

Copying from an image you never build

COPY --from isn't limited to stages defined in this Dockerfile — it accepts any image reference. A common pattern: pulling a single binary or config file out of a public image without installing anything to get it.

dockerfile
FROM alpine:3.20
COPY --from=golang:1.23 /usr/local/go/bin/gofmt /usr/local/bin/gofmt
COPY --from=nginx:1.27 /etc/nginx/nginx.conf /etc/nginx/nginx.conf.default

Branching from a shared base stage

A stage can be built on top of another stage instead of a registry image — just put that stage's name after FROM. This is how one Dockerfile serves both a development and a production image: a shared base stage with common setup, then two stages that each extend it differently.

dockerfile
FROM node:22-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./

FROM base AS development
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]

FROM base AS production
RUN npm ci --omit=dev
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]

Building (or stopping at) just one stage

--target tells docker build to stop at a specific stage, ignoring anything after it. Paired with the branching pattern above, it's how a single Dockerfile produces either image on demand.

terminal
docker build --target development -t myapp:dev .
docker build --target production -t myapp:prod .

It's also a fast way to debug a build stage directly — build just that stage, then shell into it to see the filesystem exactly as that stage left it.

terminal
docker build --target build -t debug-build .
docker run -it debug-build sh

Stages build in parallel when they don't depend on each other

BuildKit (the default builder since Docker 23) resolves the whole Dockerfile into a dependency graph before building anything. Stages that don't COPY --from each other build concurrently, and any stage nothing ever copies from — a leftover debug stage, for instance — is skipped entirely rather than wasting time building it.

.dockerignore

Lives next to the Dockerfile and excludes files from the build context, the same way .gitignore excludes files from git. Keeping node_modules and .git out shrinks the context Docker has to send to the build and avoids accidentally baking in local-only files.

.dockerignore
node_modules
.git
.env
Dockerfile
*.md