Web Development

Docker Containerization for Node.js: A Production Deep Dive

December 16, 2025 Waqas Ahmed 14 min
Docker Containerization for Node.js: A Production Deep Dive

Why Containerization Changes Node.js Deployment

Running Node.js applications directly on servers creates environment parity problems, dependency conflicts, and deployment friction that compounds as teams grow. Docker solves these by packaging your application with its exact runtime environment. For Next.js specifically, the combination of server-side rendering, API routes, and static assets creates containerization challenges that a naive Dockerfile handles poorly. This guide covers production-grade Docker patterns for Next.js — multi-stage builds, compose orchestration, nginx integration, and CI/CD wiring.

Multi-Stage Dockerfile for Next.js

A single-stage Dockerfile for Next.js results in a container image exceeding 1GB because it includes build tools, dev dependencies, and intermediate build artifacts. Multi-stage builds solve this:

# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build

# Stage 3: Runner
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]

This produces an image under 200MB. The runner stage contains only the compiled output and production Node.js runtime. Enable the standalone output option in next.config.mjs to make this work: output: 'standalone'.

Docker Compose for Local Development

# docker-compose.yml
version: '3.8'
services:
  app:
    build:
      context: .
      target: runner
    ports:
      - '3000:3000'
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp"]
      interval: 10s
      timeout: 5s
      retries: 5

  nginx:
    image: nginx:alpine
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - app

volumes:
  postgres_data:

Environment Variables Best Practices

Never bake secrets into Docker images. Use .env files with docker-compose for local development and proper secrets management (Docker Secrets, Kubernetes Secrets, or a secrets manager like Vault/AWS Secrets Manager) in production. Distinguish between build-time and runtime environment variables in Next.js — NEXT_PUBLIC_ variables are embedded at build time and cannot be changed without a rebuild. Server-side secrets should only be runtime variables never prefixed with NEXT_PUBLIC_.

Health Checks

Add a health check endpoint to your Next.js application and configure Docker to use it:

// pages/api/health.ts
export default function handler(req, res) {
  res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
}
# In docker-compose.yml app service
healthcheck:
  test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s

Nginx Reverse Proxy Configuration

upstream nextjs {
  server app:3000;
}

server {
  listen 80;
  server_name yourdomain.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  server_name yourdomain.com;
  ssl_certificate /etc/nginx/certs/fullchain.pem;
  ssl_certificate_key /etc/nginx/certs/privkey.pem;

  location /_next/static/ {
    proxy_pass http://nextjs;
    add_header Cache-Control "public, max-age=31536000, immutable";
  }

  location / {
    proxy_pass http://nextjs;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
  }
}

Docker Networks and Volume Mounting

Use named networks for service isolation. Your application container should only communicate with the database via an internal network — never expose PostgreSQL ports externally. Mount source code as volumes during development for hot reload, but use COPY in production builds for immutability.

CI/CD Integration

In GitHub Actions, build and push your Docker image to a container registry on every main branch push:

- name: Build and push Docker image
  uses: docker/build-push-action@v5
  with:
    context: .
    target: runner
    push: true
    tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

Deploy to your VPS via SSH by pulling the new image and restarting the compose stack. Use rolling restarts or blue-green deployment to achieve zero-downtime deployments even on a single-server setup.

Monitoring with docker stats

For basic monitoring, docker stats provides real-time CPU, memory, network, and disk I/O per container. For production alerting, integrate with Netdata or Prometheus via the Docker metrics endpoint. Set memory limits in docker-compose to prevent a runaway process from taking down the entire server: mem_limit: 512m for the Next.js container is a reasonable starting point, adjusted based on profiling.

Docker containerization for Next.js is a discipline, not just a Dockerfile. The patterns above — multi-stage builds, health checks, secrets management, nginx integration — form the foundation of a deployment pipeline that scales from a $5 VPS to a Kubernetes cluster without architectural rework.

#Docker#Node.js#DevOps#Containers