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.
FROM node:22-alpineWORKDIR
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.
WORKDIR /appCOPY
Copies files from the build context (the directory you ran docker build from) into the image.
COPY package.json package-lock.json ./
COPY . .
COPY . .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.
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.
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.
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.
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.
ENV NODE_ENV=production
ENV PORT=3000ARG
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.
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-alpineEXPOSE
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.
EXPOSE 3000VOLUME
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.
VOLUME /var/lib/postgresql/dataUSER
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.
RUN addgroup -S app && adduser -S app -G app
USER appLABEL
Attaches arbitrary metadata to the image as key-value pairs — maintainer, source repo, version — queryable with docker inspect.
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.
HEALTHCHECK \
CMD wget -qO- http://localhost:3000/health || exit 1ONBUILD
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.
ONBUILD COPY . /appSTOPSIGNAL
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.
STOPSIGNAL SIGINTSHELL
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.
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.
# --- 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 /app/dist ./dist
COPY /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.
FROM node:22 AS build
# ...
FROM node:22-alpine
COPY /app/dist ./dist # same as --from=buildCopying 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.
FROM alpine:3.20
COPY /usr/local/go/bin/gofmt /usr/local/bin/gofmt
COPY /etc/nginx/nginx.conf /etc/nginx/nginx.conf.defaultBranching 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.
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.
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.
docker build --target build -t debug-build .
docker run -it debug-build shStages 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.
node_modules
.git
.env
Dockerfile
*.md