Next.js course Β· Module 9: Integrations and Advanced Features
Next.js i Docker - Konteneryzacja Aplikacji
In this lesson14
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
- Environment consistency - the application runs identically on every system
- Isolation - each application has its own dedicated environment
- Portability - easy migration between environments
- Scalability - simple horizontal scaling
- 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 /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"]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.coverageNext.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 = nextConfigDocker 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 /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"]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-keyDocker 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 /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"]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 /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"]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 loggerHealth 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 hereScaling 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: LoadBalancerBest 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 curl2. Using .dockerignore
1# .dockerignore
2node_modules
3.git
4.gitignore
5README.md
6Dockerfile
7.dockerignore
8npm-debug.log
9coverage
10.nyc_output3. 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:latest4. 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
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"
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.
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is NOT a direct benefit of containerizing Next.js with Docker?
2. Which practice is INCORRECT for production deployment of Next.js in Docker?
Hands-on tasks in the game
- Vertical ordering
Arrange the steps of containerizing a Next.js application with Docker
- Click in order
Arrange the steps of Docker orchestration in order.
- Vertical ordering
Arrange the correct order of basic directives in a Dockerfile for a Next.js application:
- Code editor
Write a multi-stage Dockerfile for Next.js: install the dependencies and build the app in a stage based on a node image, copy only the build output into the final stage (COPY --from) and start the app with CMD.