Overview
Order instructions from least to most frequently changed so the layer cache survives code edits. Build and run commands are in docker-commands.
Instructions
Prefer COPY over ADD, exec form over shell form, and a non-root USER.
| Instruction | Syntax | Notes |
|---|---|---|
FROM | FROM node:24-alpine AS build | Starts a stage. Pin a digest (@sha256:...) for reproducible builds. |
ARG | ARG VERSION=1.0 | Build-time only; promote to runtime with ENV VERSION=$VERSION. Declared before FROM, it is usable only in FROM. |
ENV | ENV NODE_ENV=production | Persists into the running container. |
WORKDIR | WORKDIR /app | Sets the directory; creates it if missing. |
COPY | COPY --chown=node:node src/ ./src/ | Copies from the context or --from=<stage>. Prefer over ADD. |
ADD | ADD app.tar.gz /opt/ | Like COPY plus tar extraction and URLs; use only for those. |
LABEL | LABEL org.opencontainers.image.source="https://github.com/org/repo" | OCI image metadata; shown by docker inspect. |
RUN | RUN npm ci | Runs at build time and creates a layer. |
USER | USER node | Non-root user for later RUN, CMD, ENTRYPOINT. |
EXPOSE | EXPOSE 8080 | Documentation only; publish with docker run -p. |
HEALTHCHECK | HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD curl -fs localhost:8080/health || exit 1 | Option defaults: interval 30s, timeout 30s, start-period 0s, retries 3. |
CMD | CMD ["node", "server.js"] | Default command; replaced by docker run arguments. |
ENTRYPOINT | ENTRYPOINT ["python", "-m", "app"] | Fixed executable; CMD supplies default arguments. |
Multi-stage example
Copy only the build output and production dependencies into the final stage.
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:24-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build --chown=node:node /app/dist ./dist
COPY --from=build --chown=node:node /app/node_modules ./node_modules
USER node
EXPOSE 8080
CMD ["node", "dist/server.js"]Add a stage that runs tests and build it with docker build --target test . in CI.
Cache and layers
Copy dependency manifests before source so code edits keep the install layer cached.
# Manifests first: the install layer is reused until dependencies change
COPY package*.json ./
RUN npm ci
# apt: update and install in one layer, then clean the lists
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
# Secrets: mounted for one step, never stored in a layer
RUN --mount=type=secret,id=npm,target=/root/.npmrc npm ciGotchas
Avoid these; each breaks caching, signal handling, or secrecy.
| Gotcha | Fix |
|---|---|
Shell form CMD npm start runs under /bin/sh -c, so SIGTERM does not reach your process. | Use exec form and start the program itself: CMD ["node", "dist/server.js"]; npm as PID 1 does not forward signals reliably. |
COPY ignores USER; files are owned by root. | Add --chown=user:group. |
An ARG declared before FROM is gone inside the stage. | Redeclare it after FROM. |
ENV and ARG values show in docker history and docker inspect. | Use RUN --mount=type=secret for credentials. |
COPY . . copies .env, .git, and node_modules. | Keep a .dockerignore. |
apt-get update in its own layer goes stale in the cache. | Chain it with the install. |