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.
| Instruction | Syntax | Notes |
|---|---|---|
FROM | FROM <image>:<tag> [AS <name>] | First instruction in every stage. Pin a digest for reproducibility. |
LABEL | LABEL key="value" | OCI image metadata; queryable with docker inspect. |
ARG | ARG <name>[=<default>] | Build-time variable; not available at runtime. Declare before FROM to use in FROM. |
WORKDIR | WORKDIR /app | Set working directory; creates the path if it does not exist. |
USER | USER <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 nodePin 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.
| Instruction | Syntax | Notes |
|---|---|---|
COPY | COPY [--chown=user:group] <src> <dest> | Preferred over ADD; no hidden extraction or URL fetching. |
ADD | ADD <src> <dest> | Use only when you need automatic tar extraction; otherwise use COPY. |
RUN | RUN <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.
| Instruction | Syntax | Visible at runtime? |
|---|---|---|
ENV | ENV KEY=value | Yes; in docker inspect and process env. |
ARG | ARG KEY=default | No; 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=8080Do 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.
| Instruction | Syntax | Purpose |
|---|---|---|
CMD | CMD ["node", "server.js"] | Default arguments; overridden by docker run arguments. |
ENTRYPOINT | ENTRYPOINT ["node"] | Fixed executable; docker run arguments are appended. |
| Combined | ENTRYPOINT + CMD | CMD 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 4Always 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.
| Instruction | Syntax | Notes |
|---|---|---|
EXPOSE | EXPOSE <port>[/tcp|udp] | Documents the port; does not publish it. Publish with docker run -p. |
HEALTHCHECK | HEALTHCHECK [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 1Options 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.dockerignoreexcludes them. Always maintain a.dockerignorealongside the Dockerfile.- Each
ENVinstruction adds a layer. Combine related variables into a singleENVinstruction. RUN apt-get updatewithoutapt-get installin the sameRUNlayer leaves a stale package list cached. Always chain them.- Shell form
CMD npm startruns as/bin/sh -c "npm start", makingnpmPID 1.SIGTERMgoes to the shell, not to node. The shell often does not forward it, causing a 10-second graceful-shutdown timeout. Use exec form. USER nodebeforeCOPYmeans files are owned by the build user. Use--chown=node:nodeonCOPYto set ownership in one layer.ARGvalues beforeFROMare not in scope inside the build stage unless re-declared. DeclareARGagain afterFROMif you need it.