Docker & Containers
Docker containers package applications with their dependencies into isolated, reproducible units. Containers solve the "works on my machine" problem by bundling the runtime, libraries, and configuration into a single image that runs identically everywhere.
For web applications, Docker provides consistency across development, CI, and production environments, simplifies scaling, and enables modern deployment patterns like orchestrators (Kubernetes, ECS) and serverless containers.
A Dockerfile is a text document containing instructions to build a Docker image. Each instruction creates a layer in the image, and layers are cached to speed up subsequent builds.
| 1 | # Base image — pin to specific version, never use "latest" |
| 2 | FROM node:20-slim AS base |
| 3 | |
| 4 | # Set working directory |
| 5 | WORKDIR /app |
| 6 | |
| 7 | # Copy package files first (layer caching) |
| 8 | COPY package.json package-lock.json ./ |
| 9 | |
| 10 | # Install production dependencies only |
| 11 | RUN npm ci --only=production |
| 12 | |
| 13 | # Copy application source |
| 14 | COPY . . |
| 15 | |
| 16 | # Build the application |
| 17 | RUN npm run build |
| 18 | |
| 19 | # Expose the port |
| 20 | EXPOSE 3000 |
| 21 | |
| 22 | # Set the runtime user (security best practice) |
| 23 | USER node |
| 24 | |
| 25 | # Start the application |
| 26 | CMD ["node", "dist/server.js"] |
info
Multi-stage builds use multiple FROM statements to separate build-time dependencies from the runtime image. The result is dramatically smaller production images — a Next.js app can go from 2GB to under 200MB.
| 1 | # Stage 1: Install dependencies |
| 2 | FROM node:20-slim AS deps |
| 3 | WORKDIR /app |
| 4 | COPY package.json package-lock.json ./ |
| 5 | RUN npm ci |
| 6 | |
| 7 | # Stage 2: Build the application |
| 8 | FROM node:20-slim AS builder |
| 9 | WORKDIR /app |
| 10 | COPY --from=deps /app/node_modules ./node_modules |
| 11 | COPY . . |
| 12 | RUN npm run build |
| 13 | |
| 14 | # Stage 3: Production runtime (minimal) |
| 15 | FROM node:20-slim AS runner |
| 16 | WORKDIR /app |
| 17 | |
| 18 | ENV NODE_ENV=production |
| 19 | |
| 20 | # Copy only production artifacts |
| 21 | COPY --from=builder /app/public ./public |
| 22 | COPY --from=builder /app/.next/standalone ./ |
| 23 | COPY --from=builder /app/.next/static ./.next/static |
| 24 | |
| 25 | EXPOSE 3000 |
| 26 | USER node |
| 27 | CMD ["node", "server.js"] |
Image size comparison for a typical Next.js app:
| Approach | Image Size | Build Time |
|---|---|---|
| Single-stage (dev deps included) | ~2.1 GB | ~60s |
| Single-stage (production deps only) | ~800 MB | ~50s |
| Multi-stage (runner only) | ~150 MB | ~60s |
best practice
The .dockerignore file excludes files and directories from the Docker build context, reducing image size and preventing secrets from leaking into layers.
| 1 | # Git |
| 2 | .git |
| 3 | .gitignore |
| 4 | |
| 5 | # Dependencies (reinstalled in Docker) |
| 6 | node_modules |
| 7 | |
| 8 | # Next.js |
| 9 | .next |
| 10 | |
| 11 | # Environment files (may contain secrets) |
| 12 | .env |
| 13 | .env.local |
| 14 | .env.*.local |
| 15 | |
| 16 | # IDE |
| 17 | .idea |
| 18 | .vscode |
| 19 | *.swp |
| 20 | |
| 21 | # OS files |
| 22 | .DS_Store |
| 23 | Thumbs.db |
| 24 | |
| 25 | # Logs |
| 26 | npm-debug.log* |
| 27 | yarn-debug.log* |
| 28 | yarn-error.log* |
| 29 | |
| 30 | # Testing |
| 31 | coverage |
| 32 | __tests__ |
| 33 | *.test.js |
| 34 | *.spec.js |
danger
Docker Compose defines multi-container applications using a YAML file. It is ideal for local development environments that require databases, caches, or message queues alongside your application.
| 1 | version: "3.9" |
| 2 | |
| 3 | services: |
| 4 | app: |
| 5 | build: |
| 6 | context: . |
| 7 | dockerfile: Dockerfile.dev |
| 8 | ports: |
| 9 | - "3000:3000" |
| 10 | volumes: |
| 11 | - .:/app |
| 12 | - /app/node_modules # Anonymous volume for node_modules |
| 13 | environment: |
| 14 | - NODE_ENV=development |
| 15 | - DATABASE_URL=postgres://user:pass@db:5432/app |
| 16 | - REDIS_URL=redis://cache:6379 |
| 17 | depends_on: |
| 18 | - db |
| 19 | - cache |
| 20 | |
| 21 | db: |
| 22 | image: postgres:16-alpine |
| 23 | volumes: |
| 24 | - pgdata:/var/lib/postgresql/data |
| 25 | environment: |
| 26 | POSTGRES_USER: user |
| 27 | POSTGRES_PASSWORD: pass |
| 28 | POSTGRES_DB: app |
| 29 | ports: |
| 30 | - "5432:5432" |
| 31 | |
| 32 | cache: |
| 33 | image: redis:7-alpine |
| 34 | ports: |
| 35 | - "6379:6379" |
| 36 | |
| 37 | volumes: |
| 38 | pgdata: |
Start the development environment with:
| 1 | # Build and start all services |
| 2 | docker compose up --build |
| 3 | |
| 4 | # Run in detached mode |
| 5 | docker compose up -d |
| 6 | |
| 7 | # View logs |
| 8 | docker compose logs -f app |
| 9 | |
| 10 | # Execute commands in the app container |
| 11 | docker compose exec app npm run migrate |
| 12 | |
| 13 | # Stop and remove all containers and volumes |
| 14 | docker compose down -v |
Container registries store and distribute Docker images. Push your built images to a registry so they can be pulled by CI/CD systems and deployment targets.
| Registry | Public/Private | Integration |
|---|---|---|
| Docker Hub | Both | Default, largest community |
| GitHub Container Registry (GHCR) | Both | Native GitHub Actions integration |
| AWS ECR | Private | AWS native, IAM auth |
| Google Artifact Registry | Private | GCP native |
| 1 | # Build and tag an image |
| 2 | docker build -t my-app:latest . |
| 3 | docker tag my-app:latest ghcr.io/myorg/my-app:v1.0.0 |
| 4 | |
| 5 | # Login to GitHub Container Registry |
| 6 | echo $PAT | docker login ghcr.io -u myorg --password-stdin |
| 7 | |
| 8 | # Push to registry |
| 9 | docker push ghcr.io/myorg/my-app:v1.0.0 |
| 10 | |
| 11 | # Pull and run |
| 12 | docker pull ghcr.io/myorg/my-app:v1.0.0 |
| 13 | docker run -p 3000:3000 ghcr.io/myorg/my-app:v1.0.0 |
pro tip
Next.js applications benefit from specific Docker configurations. The official Next.js Docker example uses output file tracing to copy only required files into the production image.
| 1 | FROM node:20-slim AS base |
| 2 | WORKDIR /app |
| 3 | |
| 4 | # Dependencies |
| 5 | FROM base AS deps |
| 6 | COPY package.json package-lock.json ./ |
| 7 | RUN npm ci |
| 8 | |
| 9 | # Builder |
| 10 | FROM base AS builder |
| 11 | COPY --from=deps /app/node_modules ./node_modules |
| 12 | COPY . . |
| 13 | RUN npm run build |
| 14 | |
| 15 | # Runner |
| 16 | FROM base AS runner |
| 17 | ENV NODE_ENV=production |
| 18 | ENV NEXT_TELEMETRY_DISABLED=1 |
| 19 | |
| 20 | RUN addgroup --system --gid 1001 nodejs |
| 21 | RUN adduser --system --uid 1001 nextjs |
| 22 | |
| 23 | # Copy standalone output (Next.js output: "standalone") |
| 24 | COPY --from=builder /app/.next/standalone ./ |
| 25 | COPY --from=builder /app/public ./public |
| 26 | COPY --from=builder /app/.next/static ./.next/static |
| 27 | |
| 28 | USER nextjs |
| 29 | EXPOSE 3000 |
| 30 | CMD ["node", "server.js"] |
Enable standalone output in next.config.js:
| 1 | // next.config.js |
| 2 | /** @type {import('next').NextConfig} */ |
| 3 | const nextConfig = { |
| 4 | output: "standalone", |
| 5 | // ... other config |
| 6 | }; |
| 7 | |
| 8 | module.exports = nextConfig; |