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.
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:
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 /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 /app/public ./public
43
44# Kopiowanie zbudowanej aplikacji
45COPY /app/.next/standalone ./
46COPY /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"]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.coverageTo 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 = nextConfig1# 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: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"]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 /app/.next/standalone ./
26COPY /app/.next/static ./.next/static
27COPY /app/public ./public
28
29USER nextjs
30
31EXPOSE 3000
32
33ENV PORT 3000
34ENV HOSTNAME "0.0.0.0"
35
36HEALTHCHECK \
37 CMD curl -f http://localhost:3000/api/health || exit 1
38
39CMD ["node", "server.js"]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-key1# 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: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 /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 /app/public ./public
41COPY /app/.next/standalone ./
42COPY /app/.next/static ./.next/static
43
44USER nextjs
45
46EXPOSE 3000
47
48ENV PORT 3000
49
50CMD ["node", "server.js"]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 /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 /app/.next/standalone ./
42COPY /app/.next/static ./.next/static
43
44# Set the user
45USER nextjs
46
47# Healthcheck for monitoring
48HEALTHCHECK \
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"]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 logger1// 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}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 here1# 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: LoadBalancer1# 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 curl1# .dockerignore
2node_modules
3.git
4.gitignore
5README.md
6Dockerfile
7.dockerignore
8npm-debug.log
9coverage
10.nyc_output1# 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:latest1// 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}Problem with npm ci on Alpine Linux
1# Solution: install python and make
2RUN apk add --no-cache python3 make g++Issues with Next.js Image Optimization
1// next.config.js
2module.exports = {
3 images: {
4 unoptimized: true, // For environments without sharp
5 },
6}Memory issues
1# Limit Node.js memory
2ENV NODE_OPTIONS="--max-old-space-size=1024"Docker combined with Next.js is a powerful pairing that enables:
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.