Overview

A Dockerfile is a sequential list of instructions that Docker executes to produce a layer-cached image. Each instruction that creates filesystem changes adds a layer; minimize layers by chaining RUN commands and ordering instructions from least-changed to most-changed for maximum cache reuse. For the commands that build and run images, see docker-commands.

Base and metadata instructions

Set the base image, author metadata, and the working context before any filesystem instructions.

InstructionSyntaxNotes
FROMFROM <image>:<tag> [AS <name>]First instruction in every stage. Pin a digest for reproducibility.
LABELLABEL key="value"OCI image metadata; queryable with docker inspect.
ARGARG <name>[=<default>]Build-time variable; not available at runtime. Declare before FROM to use in FROM.
WORKDIRWORKDIR /appSet working directory; creates the path if it does not exist.
USERUSER <user>[:<group>]Switch to a non-root user; applies to all subsequent RUN, CMD, ENTRYPOINT.
FROM node:22-alpine AS base
LABEL org.opencontainers.image.source="https://github.com/org/repo"
ARG NODE_ENV=production
WORKDIR /app
USER node

Pin base images to a digest (node:22-alpine@sha256:...) in production to prevent silent upstream changes.

Filesystem instructions

Copy files into the image and run commands to build the application.

InstructionSyntaxNotes
COPYCOPY [--chown=user:group] <src> <dest>Preferred over ADD; no hidden extraction or URL fetching.
ADDADD <src> <dest>Use only when you need automatic tar extraction; otherwise use COPY.
RUNRUN <command>Creates a layer; chain with && to minimize layers. Use RUN --mount for secrets.
# Good: chain RUN commands to minimize layers; clean caches in same layer
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*
 
# COPY dependency manifests first to cache the install layer
COPY package*.json ./
RUN npm ci --omit=dev
 
# COPY source after installs so code changes don't bust the npm install layer
COPY --chown=node:node . .

Environment variables

ENV persists into the container at runtime. ARG is build-only.

InstructionSyntaxVisible at runtime?
ENVENV KEY=valueYes; in docker inspect and process env.
ARGARG KEY=defaultNo; only during build.
ARG APP_VERSION=0.0.0           # build-time only
ENV APP_VERSION=$APP_VERSION    # promote to runtime if needed
ENV NODE_ENV=production \
    PORT=8080

Do not ENV secrets into images; they appear in docker history and docker inspect. Use runtime --env-file or a secrets manager.

Entrypoint and CMD

These two instructions define what runs when the container starts.

InstructionSyntaxPurpose
CMDCMD ["node", "server.js"]Default arguments; overridden by docker run arguments.
ENTRYPOINTENTRYPOINT ["node"]Fixed executable; docker run arguments are appended.
CombinedENTRYPOINT + CMDCMD provides defaults for ENTRYPOINT arguments.
# CMD only: easily overridden
CMD ["npm", "start"]
 
# ENTRYPOINT + CMD: ENTRYPOINT is fixed; CMD is default argument
ENTRYPOINT ["python", "-m", "gunicorn"]
CMD ["app:app", "--bind", "0.0.0.0:8080"]
 
# Override CMD at runtime: docker run myimage app:app --workers 4

Always use the exec form (["executable", "arg"]), not the shell form (CMD npm start). The shell form wraps the command in sh -c, adding a shell process that does not forward signals.

Network and health

Declare the port the application listens on and define a health probe.

InstructionSyntaxNotes
EXPOSEEXPOSE <port>[/tcp|udp]Documents the port; does not publish it. Publish with docker run -p.
HEALTHCHECKHEALTHCHECK [options] CMD <command>Sets the container health status used by orchestrators.
EXPOSE 8080
 
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD curl -fs http://localhost:8080/health || exit 1

Options for HEALTHCHECK: --interval (default 30s), --timeout (default 30s), --start-period (default 0s), --retries (default 3).

Multi-stage build pattern

Multi-stage builds produce a small runtime image by discarding build-time dependencies.

# Stage 1: install and build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# Stage 2: run tests in the build stage (target in CI)
FROM builder AS test
RUN npm test
 
# Stage 3: minimal runtime
FROM node:22-alpine AS runtime
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
USER app
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
EXPOSE 8080
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:8080/health || exit 1
CMD ["node", "dist/server.js"]

Build just the test stage in CI: docker build --target test -t myapp:test .

Common gotchas

  • COPY . . copies .env, node_modules, and secrets unless .dockerignore excludes them. Always maintain a .dockerignore alongside the Dockerfile.
  • Each ENV instruction adds a layer. Combine related variables into a single ENV instruction.
  • RUN apt-get update without apt-get install in the same RUN layer leaves a stale package list cached. Always chain them.
  • Shell form CMD npm start runs as /bin/sh -c "npm start", making npm PID 1. SIGTERM goes to the shell, not to node. The shell often does not forward it, causing a 10-second graceful-shutdown timeout. Use exec form.
  • USER node before COPY means files are owned by the build user. Use --chown=node:node on COPY to set ownership in one layer.
  • ARG values before FROM are not in scope inside the build stage unless re-declared. Declare ARG again after FROM if you need it.