We use cookies to enhance your experience on the site
CodeWorlds

Next.js i Docker - Konteneryzacja Aplikacji

Just as ancient builders used standard molds to create identical building bricks, modern Docker lets us create standard, portable containers for our Next.js applications. In this lesson we will learn how to "package" our application into a Docker container that will run the same way in any environment - from a developer's local machine to production servers.

What is Docker and why is it useful for Next.js?

Docker is an application containerization platform that lets you package an application together with all its dependencies into lightweight, portable containers. For Next.js applications Docker offers many benefits:

Advantages of using Docker with Next.js

  1. Environment consistency - the application runs identically on every system
  2. Isolation - each application has its own dedicated environment
  3. Portability - easy migration between environments
  4. Scalability - simple horizontal scaling
  5. Versioning - ability to manage environment versions

Docker basics for Next.js

Docker file structure

Let's start by creating the basic Docker files in our Next.js project:

1# Dockerfile
2# Stage 1: Install dependencies
3FROM node:18-alpine AS deps
4
5WORKDIR /app
6
7# Copy package files
8COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
9
10# Install dependencies based on the lockfile
11RUN \
12  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
13  elif [ -f package-lock.json ]; then npm ci; \
14  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
15  else echo "Lockfile not found." && exit 1; \
16  fi
17
18# Stage 2: Build the application
19FROM node:18-alpine AS builder
20
21WORKDIR /app
22
23# Copy dependencies from the previous stage
24COPY --from=deps /app/node_modules ./node_modules
25
26# Copy the source code
27COPY . .
28
29# Build the Next.js application
30RUN npm run build
31
32# Stage 3: Production environment
33FROM node:18-alpine AS runner
34
35WORKDIR /app
36
37# Add non-root user
38RUN addgroup --system --gid 1001 nodejs
39RUN adduser --system --uid 1001 nextjs
40
41# Copy public files
42COPY --from=builder /app/public ./public
43
44# Kopiowanie zbudowanej aplikacji
45COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
46COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
47
48USER nextjs
49
50EXPOSE 3000
51
52ENV PORT 3000
53ENV HOSTNAME "0.0.0.0"
54
55CMD ["node", "server.js"]

Plik .dockerignore

Similar to

.gitignore
, the
.dockerignore
file specifies which files should be ignored when building a Docker image:

1# .dockerignore
2Dockerfile
3.dockerignore
4node_modules
5npm-debug.log
6README.md
7.env
8.git
9.gitignore
10.next
11.nyc_output
12coverage
13.coverage

Next.js configuration for Docker

Standalone Output

To optimize Docker image size, Next.js offers the

standalone
output option:

1// next.config.js
2/** @type {import('next').NextConfig} */
3const nextConfig = {
4  output: 'standalone',
5  experimental: {
6    // Optimizations for Docker
7    outputFileTracingRoot: path.join(__dirname, '../../'),
8  },
9  // Environment variables configuration
10  env: {
11    CUSTOM_KEY: process.env.CUSTOM_KEY,
12  },
13  // Image optimization
14  images: {
15    domains: ['example.com'],
16    unoptimized: process.env.NODE_ENV === 'development',
17  },
18}
19
20module.exports = nextConfig

Docker Compose for development environments

Basic Docker Compose configuration

1# docker-compose.yml
2version: '3.8'
3
4services:
5  app:
6    build:
7      context: .
8      dockerfile: Dockerfile
9      target: development
10    ports:
11      - "3000:3000"
12    environment:
13      - NODE_ENV=development
14    volumes:
15      - .:/app
16      - /app/node_modules
17      - /app/.next
18    command: npm run dev
19
20  database:
21    image: postgres:15
22    environment:
23      POSTGRES_DB: nextjs_app
24      POSTGRES_USER: user
25      POSTGRES_PASSWORD: password
26    ports:
27      - "5432:5432"
28    volumes:
29      - postgres_data:/var/lib/postgresql/data
30
31  redis:
32    image: redis:7-alpine
33    ports:
34      - "6379:6379"
35
36volumes:
37  postgres_data:

Dockerfile for the development environment

1# Dockerfile.dev
2FROM node:18-alpine
3
4WORKDIR /app
5
6# Install global dependencies
7RUN npm install -g nodemon
8
9# Copy package files
10COPY package*.json ./
11
12# Install dependencies
13RUN npm ci
14
15# Copy source code
16COPY . .
17
18EXPOSE 3000
19
20CMD ["npm", "run", "dev"]

Multi-stage Dockerfile for production

Optimized Dockerfile

1# Dockerfile.prod
2# Build stage
3FROM node:18-alpine AS build
4
5WORKDIR /app
6
7# Install dependencies
8COPY package*.json ./
9RUN npm ci --only=production && npm cache clean --force
10
11# Copy and build the application
12COPY . .
13RUN npm run build
14
15# Production stage
16FROM node:18-alpine AS production
17
18WORKDIR /app
19
20# Create user
21RUN addgroup -g 1001 -S nodejs
22RUN adduser -S nextjs -u 1001
23
24# Copy only the necessary files
25COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
26COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
27COPY --from=build --chown=nextjs:nodejs /app/public ./public
28
29USER nextjs
30
31EXPOSE 3000
32
33ENV PORT 3000
34ENV HOSTNAME "0.0.0.0"
35
36HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
37  CMD curl -f http://localhost:3000/api/health || exit 1
38
39CMD ["node", "server.js"]

Managing environment variables

Environment file

1# .env.docker
2NODE_ENV=production
3NEXT_PUBLIC_API_URL=https://api.example.com
4DATABASE_URL=postgresql://user:password@database:5432/nextjs_app
5REDIS_URL=redis://redis:6379
6JWT_SECRET=your-secret-key

Docker Compose with environment files

1# docker-compose.prod.yml
2version: '3.8'
3
4services:
5  app:
6    build:
7      context: .
8      dockerfile: Dockerfile.prod
9    ports:
10      - "3000:3000"
11    env_file:
12      - .env.docker
13    depends_on:
14      - database
15      - redis
16    restart: unless-stopped
17
18  nginx:
19    image: nginx:alpine
20    ports:
21      - "80:80"
22      - "443:443"
23    volumes:
24      - ./nginx.conf:/etc/nginx/nginx.conf
25      - ./ssl:/etc/nginx/ssl
26    depends_on:
27      - app
28    restart: unless-stopped
29
30  database:
31    image: postgres:15
32    env_file:
33      - .env.docker
34    volumes:
35      - postgres_data:/var/lib/postgresql/data
36    restart: unless-stopped
37
38  redis:
39    image: redis:7-alpine
40    restart: unless-stopped
41
42volumes:
43  postgres_data:

Image optimization Docker

Reducing image size

1# Dockerfile.optimized
2FROM node:18-alpine AS base
3
4# Install only the required packages
5RUN apk add --no-cache libc6-compat
6
7FROM base AS deps
8WORKDIR /app
9
10# Copy dependency files
11COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
12
13# Install dependencies
14RUN \
15  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
16  elif [ -f package-lock.json ]; then npm ci; \
17  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \
18  else echo "Lockfile not found." && exit 1; \
19  fi
20
21FROM base AS builder
22WORKDIR /app
23
24COPY --from=deps /app/node_modules ./node_modules
25COPY . .
26
27# Build with optimizations
28ENV NEXT_TELEMETRY_DISABLED 1
29RUN npm run build
30
31FROM base AS runner
32WORKDIR /app
33
34ENV NODE_ENV production
35ENV NEXT_TELEMETRY_DISABLED 1
36
37RUN addgroup --system --gid 1001 nodejs
38RUN adduser --system --uid 1001 nextjs
39
40COPY --from=builder /app/public ./public
41COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
42COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
43
44USER nextjs
45
46EXPOSE 3000
47
48ENV PORT 3000
49
50CMD ["node", "server.js"]

Security in Docker containers

Security best practices

1# Dockerfile.secure
2FROM node:18-alpine AS base
3
4# Update system packages
5RUN apk update && apk upgrade
6
7# Install security tools
8RUN apk add --no-cache dumb-init
9
10# Use a non-admin user
11RUN addgroup -g 1001 -S nodejs
12RUN adduser -S nextjs -u 1001
13
14FROM base AS deps
15WORKDIR /app
16
17# Copy only the necessary files
18COPY package*.json ./
19
20# Install with a security audit
21RUN npm ci --only=production && npm audit fix
22
23FROM base AS builder
24WORKDIR /app
25
26COPY --from=deps /app/node_modules ./node_modules
27COPY . .
28
29# Security scan before building
30RUN npm audit
31
32# Build the application
33RUN npm run build
34
35FROM base AS runner
36WORKDIR /app
37
38ENV NODE_ENV=production
39
40# Copy with appropriate permissions
41COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
42COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
43
44# Set the user
45USER nextjs
46
47# Healthcheck for monitoring
48HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
49  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
50
51EXPOSE 3000
52
53# Use dumb-init for better process management
54ENTRYPOINT ["dumb-init", "--"]
55CMD ["node", "server.js"]

Monitoring and logging

Logging configuration

1// utils/logger.js
2import winston from 'winston'
3
4const logger = winston.createLogger({
5  level: process.env.LOG_LEVEL || 'info',
6  format: winston.format.combine(
7    winston.format.timestamp(),
8    winston.format.errors({ stack: true }),
9    winston.format.json()
10  ),
11  defaultMeta: { service: 'nextjs-app' },
12  transports: [
13    new winston.transports.File({ filename: 'error.log', level: 'error' }),
14    new winston.transports.File({ filename: 'combined.log' }),
15  ],
16})
17
18if (process.env.NODE_ENV !== 'production') {
19  logger.add(new winston.transports.Console({
20    format: winston.format.simple()
21  }))
22}
23
24export default logger

Health Check endpoint

1// pages/api/health.js
2export default function handler(req, res) {
3  const healthcheck = {
4    uptime: process.uptime(),
5    message: 'OK',
6    timestamp: Date.now(),
7    environment: process.env.NODE_ENV,
8    version: process.env.npm_package_version,
9  }
10
11  try {
12    // Check connections to database, Redis, etc.
13    res.status(200).json(healthcheck)
14  } catch (error) {
15    healthcheck.message = error.message
16    res.status(503).json(healthcheck)
17  }
18}

CI/CD with Docker

GitHub Actions with Docker

1# .github/workflows/docker.yml
2name: Docker Build and Deploy
3
4on:
5  push:
6    branches: [main]
7  pull_request:
8    branches: [main]
9
10jobs:
11  build:
12    runs-on: ubuntu-latest
13
14    steps:
15    - name: Checkout code
16      uses: actions/checkout@v3
17
18    - name: Set up Docker Buildx
19      uses: docker/setup-buildx-action@v2
20
21    - name: Login to Docker Hub
22      uses: docker/login-action@v2
23      with:
24        username: ${{ secrets.DOCKERHUB_USERNAME }}
25        password: ${{ secrets.DOCKERHUB_TOKEN }}
26
27    - name: Build and push
28      uses: docker/build-push-action@v4
29      with:
30        context: .
31        file: ./Dockerfile
32        platforms: linux/amd64,linux/arm64
33        push: true
34        tags: \
35          myapp/nextjs:latest
36          myapp/nextjs:${{ github.sha }}
37        cache-from: type=gha
38        cache-to: type=gha,mode=max
39
40  deploy:
41    needs: build
42    runs-on: ubuntu-latest
43    if: github.ref == 'refs/heads/main'
44
45    steps:
46    - name: Deploy to production
47      run: |
48        echo "Deploying to production server"
49        # Deployment code for production server goes here

Scaling and orchestration

Kubernetes Deployment

1# k8s/deployment.yml
2apiVersion: apps/v1
3kind: Deployment
4metadata:
5  name: nextjs-app
6spec:
7  replicas: 3
8  selector:
9    matchLabels:
10      app: nextjs-app
11  template:
12    metadata:
13      labels:
14        app: nextjs-app
15    spec:
16      containers:
17      - name: nextjs-app
18        image: myapp/nextjs:latest
19        ports:
20        - containerPort: 3000
21        env:
22        - name: NODE_ENV
23          value: "production"
24        - name: DATABASE_URL
25          valueFrom:
26            secretKeyRef:
27              name: db-secret
28              key: url
29        resources:
30          requests:
31            memory: "128Mi"
32            cpu: "100m"
33          limits:
34            memory: "512Mi"
35            cpu: "500m"
36        livenessProbe:
37          httpGet:
38            path: /api/health
39            port: 3000
40          initialDelaySeconds: 30
41          periodSeconds: 10
42        readinessProbe:
43          httpGet:
44            path: /api/health
45            port: 3000
46          initialDelaySeconds: 5
47          periodSeconds: 5
48---
49apiVersion: v1
50kind: Service
51metadata:
52  name: nextjs-service
53spec:
54  selector:
55    app: nextjs-app
56  ports:
57  - port: 80
58    targetPort: 3000
59  type: LoadBalancer

Best practices

1. Layer optimization

1# Good - group commands together
2RUN apk update && \
3    apk add --no-cache git curl && \
4    rm -rf /var/cache/apk/*
5
6# Bad - separate layers
7RUN apk update
8RUN apk add --no-cache git
9RUN apk add --no-cache curl

2. Using .dockerignore

1# .dockerignore
2node_modules
3.git
4.gitignore
5README.md
6Dockerfile
7.dockerignore
8npm-debug.log
9coverage
10.nyc_output

3. Managing secrets

1# Use Docker secrets instead of environment variables
2docker service create \
3  --name nextjs-app \
4  --secret db_password \
5  --secret api_key \
6  myapp/nextjs:latest

4. Monitoring and alerting

1// middleware/monitoring.js
2import prometheus from 'prom-client'
3
4const httpDuration = new prometheus.Histogram({
5  name: 'http_request_duration_seconds',
6  help: 'Duration of HTTP requests in seconds',
7  labelNames: ['method', 'route', 'status'],
8  buckets: [0.1, 0.5, 1, 2, 5]
9})
10
11export function monitoringMiddleware(req, res, next) {
12  const start = Date.now()
13  
14  res.on('finish', () => {
15    const duration = (Date.now() - start) / 1000
16    httpDuration
17      .labels(req.method, req.route?.path || req.path, res.statusCode)
18      .observe(duration)
19  })
20  
21  next()
22}

Troubleshooting

Common problems and solutions

  1. Problem with npm ci on Alpine Linux

    1# Solution: install python and make
    2RUN apk add --no-cache python3 make g++
  2. Issues with Next.js Image Optimization

    1// next.config.js
    2module.exports = {
    3  images: {
    4    unoptimized: true, // For environments without sharp
    5  },
    6}
  3. Memory issues

    1# Limit Node.js memory
    2ENV NODE_OPTIONS="--max-old-space-size=1024"

Summary

Docker combined with Next.js is a powerful pairing that enables:

  • Consistent environments across every stage of development
  • Easy scaling of applications in the cloud
  • Safe deployments with process isolation
  • Efficient management of dependencies and versions

Just as ancient builders created durable structures using standardized components, Docker lets us build reliable, scalable Next.js applications that behave the same in every environment.

In the following lessons we will explore advanced container orchestration techniques and strategies for deploying Next.js applications in cloud environments.

Go to CodeWorlds