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.log5. 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 →