← All posts

Production-ready Docker images for Node.js apps

13 October 2026 · 7 min read

Most Dockerfiles that work on a laptop are not ones you want in production. They are large, slow to rebuild, run as root and carry build tools an attacker would love to find. Five small changes fix most of that. We will use a Node.js web app as the example, but the ideas apply to any stack.

1. Use multi-stage builds

Build in one stage, run in another. The final image then contains only what the app needs at runtime: no compilers, no dev dependencies, no source maps unless you want them.

FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM deps AS build
COPY . .
RUN npm run build

FROM node:22-alpine AS prod-deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

FROM node:22-alpine AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/build ./build
COPY package.json ./
USER node
EXPOSE 3000
CMD ["npm", "start"]

2. Order layers for caching

Docker caches each instruction and invalidates everything after the first change. Copy the dependency manifests and install before copying the rest of the source. Then a code-only change reuses the cached install layer, turning minutes into seconds.

3. Do not run as root

If an attacker gains code execution in your app, running as root makes everything worse. The official Node images ship a non-root node user; switch to it with USER node, as above. Make sure any directories the app writes to are owned by that user.

4. Add a .dockerignore

Without one, COPY . . sends your whole directory to the build, including node_modules, .git and local environment files. That slows builds and can leak secrets into image layers.

node_modules
.git
.env*
build
Dockerfile
npm-debug.log

5. Pin versions and scan

  • Pin the base image to a specific major version, or a digest for fully reproducible builds
  • Rebuild regularly so base image security patches reach you
  • Scan images in CI with a tool such as Trivy or Grype and fail on high-severity findings you can fix

Health checks and signals

Orchestrators need to know when your app is ready. Expose a lightweight health endpoint and configure it as a readiness or liveness probe in Kubernetes, or a HEALTHCHECK elsewhere. Also make sure your process receives termination signals: running node directly, rather than through a shell wrapper, lets it shut down gracefully when the container is stopped.

Do not bake in secrets

Anything copied or set with ENV at build time lives in the image history. Inject secrets at runtime through your platform's secret store or environment, never through the Dockerfile.

Quick check: can you rebuild after a one-line code change in under a minute, and does docker run as a non-root user? If not, start with layer order and USER.

Want to talk it through?

Email or call us. No obligation, just a conversation about what you are trying to achieve.

Contact us →