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.

InstructionSyntaxNotes
FROMFROM node:24-alpine AS buildStarts a stage. Pin a digest (@sha256:...) for reproducible builds.
ARGARG VERSION=1.0Build-time only; promote to runtime with ENV VERSION=$VERSION. Declared before FROM, it is usable only in FROM.
ENVENV NODE_ENV=productionPersists into the running container.
WORKDIRWORKDIR /appSets the directory; creates it if missing.
COPYCOPY --chown=node:node src/ ./src/Copies from the context or --from=<stage>. Prefer over ADD.
ADDADD app.tar.gz /opt/Like COPY plus tar extraction and URLs; use only for those.
LABELLABEL org.opencontainers.image.source="https://github.com/org/repo"OCI image metadata; shown by docker inspect.
RUNRUN npm ciRuns at build time and creates a layer.
USERUSER nodeNon-root user for later RUN, CMD, ENTRYPOINT.
EXPOSEEXPOSE 8080Documentation only; publish with docker run -p.
HEALTHCHECKHEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD curl -fs localhost:8080/health || exit 1Option defaults: interval 30s, timeout 30s, start-period 0s, retries 3.
CMDCMD ["node", "server.js"]Default command; replaced by docker run arguments.
ENTRYPOINTENTRYPOINT ["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 ci

Gotchas

Avoid these; each breaks caching, signal handling, or secrecy.

GotchaFix
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.