Node.js
June 10, 20266 min read...
Node.jsJune 10, 20266 min read

Dockerizing Node.js Applications: Production Best Practices 2026

Learn how to containerize Node.js apps for production with optimized image sizes, security hardening, and efficient caching. This guide covers Dockerfile best practices, .dockerignore, non-root users, and deployment patterns.

Dockerizing Node.js Applications: Production Best Practices 2026

Docker has become the standard for packaging Node.js applications, but many developers end up with 1GB images, slow rebuilds, and security vulnerabilities. This guide collects battle-tested patterns from production deployments serving millions of requests. By the end, your images will be smaller, faster to build, and more secure.

The Golden Production Dockerfile

Start with this template and customize as needed:

# Build stage
FROM node:22-alpine AS builder
WORKDIR /app

Copy package files for layer caching

COPY package*.json ./ COPY yarn.lock ./

Install dependencies (including dev for build)

RUN npm ci --only=production=false

Copy source code

COPY . .

Build TypeScript, compile assets, etc.

RUN npm run build

Production stage

FROM node:22-alpine RUN apk add --no-cache dumb-init

Create non-root user

RUN addgroup -g 1001 -S nodejs &&
adduser -S nodejs -u 1001

WORKDIR /app

Copy only needed files from builder

COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./ COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist

Switch to non-root user

USER nodejs

Expose port

EXPOSE 3000

Health check

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3
CMD node -e “require(‘http’).get(‘http://localhost:3000/health’, ® => {process.exit(r.statusCode === 200 ? 0 : 1)})”

Use dumb-init to handle signals properly

ENTRYPOINT [“dumb-init”, “–”] CMD [“node”, “dist/server.js”]

Critical Optimizations Explained

1. Multi-stage Builds

Using two stages (builder + production) reduces final image size by 60-80%. Build tools (TypeScript, Webpack, dev dependencies) stay in the builder stage, never reaching production.

Size comparison:
Single-stage: ~450MB
Multi-stage (above): ~95MB

2. Alpine Base Images

node:22-alpine is based on Alpine Linux (5MB) instead of Debian (~180MB). Only the Node.js runtime and essential libraries are included. For native addons, you may need node:22-slim instead.

3. Layer Caching Strategy

Docker caches each RUN, COPY, and ADD instruction. The most cache-unstable operations should go at the bottom:

# ✅ GOOD: Copy package files first (rarely change vs source)
COPY package*.json ./
RUN npm ci
COPY . .          # Source changes often, but rebuilds only this layer

❌ BAD: Copy source before npm install

COPY . . RUN npm ci # Any source change invalidates cache, re-runs npm install

Essential .dockerignore

Prevent secrets and unnecessary files from entering the build context:

node_modules
npm-debug.log
.env
.git
.gitignore
README.md
.github
.nyc_output
coverage
.idea
.vscode
*.md
docker-compose*.yml
Dockerfile
dist   # Built in container
.DS_Store

Running as Non-Root User

Never run containers as root. The example above creates a nodejs user with UID 1001. This mitigates container breakout vulnerabilities.

Verify: docker run --rm your-app whoami should output nodejs, not root.

Environment Variables Configuration

Don't bake env vars into the image (except truly static ones like NODE_ENV). Use runtime injection:

# Pass at runtime
docker run -e DATABASE_URL=postgres://... -e API_KEY=secret your-app

Or use env file

docker run --env-file .env.production your-app

For Kubernetes, use Secrets

kubectl create secret generic app-secrets --from-literal=api-key=xyz

Health Checks and Graceful Shutdown

Your app must handle SIGTERM gracefully to avoid dropping requests during scaling or deployment:

// server.js
const server = app.listen(3000);

process.on(‘SIGTERM’, () => { console.log(‘SIGTERM received, closing server…’); server.close(() => { console.log(‘Server closed, exiting’); process.exit(0); });

// Force exit after 10 seconds setTimeout(() => { console.error(‘Could not close connections in time, forcing exit’); process.exit(1); }, 10000); });

Orchestrators like Kubernetes use health checks to restart unhealthy containers:

app.get('/health', (req, res) => {
  // Check database connection, etc.
  if (db.connected) {
    res.status(200).send('OK');
  } else {
    res.status(503).send('Unavailable');
  }
});

Resource Limits

Always set memory and CPU limits in your orchestration. Node.js has a default heap limit of ~2GB on 64-bit, but you should constrain per container:

# docker-compose.yml
services:
  api:
    image: your-app:latest
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

For Kubernetes:

resources:
  requests:
    memory: "256Mi"
    cpu: "250m"
  limits:
    memory: "512Mi"
    cpu: "500m"

Optimizing Startup Time

Large node_modules slow down container start. Reduce with:

  • npm ci --only=production (no dev dependencies)
  • Use --no-audit --no-fund to skip unnecessary steps
  • Consider pnpm or yarn with faster installs
  • Use --max-old-space-size to limit memory for embedded devices

Security Scanning

Before pushing to registry, scan for vulnerabilities:

# Using Docker Scout
docker scout quickview your-app:latest

Using Trivy (open source)

trivy image your-app:latest

In CI

  • name: Trivy scan uses: aquasecurity/trivy-action@master with: image-ref: your-app:latest format: ‘sarif’ exit-code: ‘1’ severity: ‘CRITICAL,HIGH’

Building for Different Environments

Use Docker build args for environment-specific configs:

ARG NODE_ENV=production
ENV NODE_ENV=$NODE_ENV
RUN if [ "$NODE_ENV" = "development" ]; then npm install --only=development; fi

Build: docker build --build-arg NODE_ENV=development -t myapp:dev .

Conclusion

The Dockerfile provided in this guide is production-ready for 95% of Node.js applications. Start from this template, then layer on your specific needs (native addons, custom build steps, etc.). Always run as non-root, set resource limits, and implement health checks. Your ops team will appreciate smaller images (<100MB) and your security team will appreciate no root processes. Containerize wisely.

Comments

Join the conversation — sign in to leave a comment.