$devtoolkit.sh/templates/dockerfile/node

Node.js Production Dockerfile Template

A production-ready Node.js Dockerfile is substantially different from the single-stage development containers used for local development. The goals are conflicting: development containers prioritize fast rebuild times and debuggability, while production containers prioritize minimal image size, security, and reproducibility. A proper production Dockerfile achieves all three through multi-stage builds.

Multi-stage builds are the core technique for lean Node.js production images. The first stage (build stage) installs all dependencies including devDependencies, runs TypeScript compilation or bundling, and generates the build artifacts. The second stage (runtime stage) starts from a fresh base image, copies only the compiled output and production dependencies, and runs the application. The final image contains no build tools, no devDependencies, no TypeScript source, and no compilation artifacts — just the running application.

Base image selection significantly affects security and image size. node:20-alpine is the preferred base for most production Node.js applications: Alpine Linux is 5MB compared to Debian's 100MB, and the Alpine base has fewer CVEs. The tradeoff is that Alpine uses musl libc instead of glibc, which causes issues with some native Node.js modules that link against glibc. If your app uses native modules, node:20-slim (Debian slim) is a better choice than node:20-alpine.

Layer caching optimization is critical for fast builds in CI. Copy your package.json and package-lock.json before copying source code. Docker rebuilds layers only when their inputs change; separating dependency installation from source copying means npm install is cached unless your dependencies actually change, which dramatically speeds up CI pipelines where source changes but dependencies remain stable.

The non-root user requirement is a security baseline for production containers. By default, Docker runs processes as root, which means a container escape vulnerability could give an attacker root access to the host system. Add a non-root user (useradd -r -u 1001 node) and switch to it before the CMD instruction. Also set appropriate file ownership so the application user can write to any directories it needs.

Health checks tell orchestrators (Kubernetes, ECS, Docker Swarm) whether the container is healthy enough to receive traffic. Include a HEALTHCHECK instruction that curls your application's health endpoint and fails if the response is not HTTP 200. This enables zero-downtime deployments.

Template Preview

{"language":"node","nodeVersion":"20","useAlpine":true,"multiStage":true,"nonRootUser":true,"healthCheck":true,"packageManager":"npm"}

Customize this template with your own details using the free generator:

Open in Generator

FAQ

Should I use Alpine or Debian slim for my Node.js Docker image?
Use Alpine (node:20-alpine) if you have no native npm modules — you get the smallest image (around 60MB total) with the fewest CVEs. Use Debian slim (node:20-slim) if any of your dependencies use native addons that link against glibc, such as bcrypt, sharp, or sqlite3. Check your production dependencies first: run npm list --depth 0 and look for packages that compile C++ addons.
What is the correct .dockerignore for a Node.js project?
Include at minimum: node_modules, .git, .env*, dist (if you clean it before building), coverage, .nyc_output, *.log, and any test fixture directories with large files. Not ignoring node_modules is the most common mistake — it causes Docker to copy hundreds of megabytes into the build context, slowing every build even if nothing has changed. The .dockerignore file must be in the same directory as your Dockerfile.
How do I handle environment variables in a Node.js Dockerfile?
Never hardcode secrets in Dockerfile ENV instructions — they are visible in docker history and image layers. Instead, pass secrets at runtime via docker run --env-file .env or, in production, through your orchestrator's secret management (Kubernetes Secrets, AWS Secrets Manager, Doppler). The Dockerfile should only set non-secret defaults like NODE_ENV=production and PORT=3000.

Related Templates

/templates/dockerfile/nodev1.0.0