Docker Guide 2026 — Containerize Node.js, Python, and Next.js Apps

Sanjeev SharmaSanjeev Sharma
4 min read

Advertisement

Introduction

Why This Matters

Docker is the standard unit of deployment in 2026. Containerizing your app guarantees identical behavior across development, CI, and production. Multi-stage builds keep images small and secure. Docker Compose replaces complex local setup scripts.

Production Dockerfile for Node.js

Multi-stage builds reduce image size by 60–80%:

# Dockerfile
FROM node:22-alpine AS base
WORKDIR /app
 
# Install dependencies only when package.json changes
FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
 
# Build stage
FROM base AS builder
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# Production stage — minimal image
FROM base AS runner
ENV NODE_ENV=production
 
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
 
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
 
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1
 
CMD ["node", "dist/server.js"]

Next.js Dockerfile

FROM node:22-alpine AS base
WORKDIR /app
 
FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci
 
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
 
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
 
FROM base AS runner
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
 
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
 
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"]
// next.config.ts — enable standalone output
const config = {
  output: 'standalone',
}
export default config

Docker Compose for Local Development

# docker-compose.yml
services:
  app:
    build:
      context: .
      target: builder   # Use build stage for hot reload
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    command: npm run dev
 
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
 
  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
 
volumes:
  postgres_data:
  redis_data:

Useful Docker Commands

# Build and tag
docker build -t my-app:1.0.0 .
docker build -t my-app:latest --target runner .
 
# Run with env file
docker run -p 3000:3000 --env-file .env my-app:latest
 
# Compose commands
docker compose up -d          # Start detached
docker compose logs -f app    # Follow app logs
docker compose exec app sh    # Shell into container
docker compose down -v        # Stop and remove volumes
 
# Inspect image layers
docker history my-app:latest
docker image inspect my-app:latest
 
# Clean up
docker system prune -af
docker volume prune -f

.dockerignore

node_modules
.next
.git
.env
.env.local
*.md
coverage
dist
.turbo

Common Mistakes

  • Not using a .dockerignore — copies node_modules into the image, bloating it by hundreds of MB
  • Running the container as root — use a non-root user with USER directive
  • Using npm install instead of npm cici installs exact versions from package-lock.json
  • Not using multi-stage builds — dev dependencies end up in the production image
  • Using latest tags for base images in production — pin to a specific version for reproducibility

Best Practices

  • Pin base image versions (node:22-alpine, not node:alpine) for reproducible builds
  • Use Alpine variants to keep images small — typically 5–50 MB vs 500+ MB for Debian
  • Add HEALTHCHECK instructions so orchestrators know when a container is truly ready
  • Store secrets in environment variables or a secrets manager, never COPY-ed into the image
  • Use Docker BuildKit (DOCKER_BUILDKIT=1) for parallel build stages and better cache control

Key Takeaways

  • Multi-stage builds separate dev dependencies from the production image, reducing size by 60–80%
  • Non-root users in Docker prevent privilege escalation if the container is compromised
  • npm ci installs exact locked dependencies — always prefer it over npm install in CI and Docker
  • output: 'standalone' in Next.js generates a self-contained production build with no extra node_modules copy needed
  • HEALTHCHECK lets Docker and Kubernetes determine when the container is accepting traffic
  • Docker Compose replaces complex README setup instructions for local development
  • .dockerignore is as important as .gitignore — always exclude node_modules, .git, and .env
  • Pin image digest (node:22-alpine@sha256:...) in production for fully reproducible builds

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading