Tak jak starożytni budowniczowie wykorzystywali standardowe formy do tworzenia identycznych cegieł budowlanych, współczesny Docker pozwala nam tworzyć standardowe, przenośne kontenery dla naszych aplikacji Next.js. W tej lekcji nauczymy się, jak "zapakować" naszą aplikację w kontener Docker, który będzie działał jednakowo na każdym środowisku - od lokalnej maszyny programisty po serwery produkcyjne.
Docker to platforma do konteneryzacji aplikacji, która pozwala na pakowanie aplikacji wraz z wszystkimi jej zależnościami w lekkie, przenośne kontenery. Dla aplikacji Next.js Docker oferuje wiele korzyści:
Zacznijmy od utworzenia podstawowych plików Docker w naszym projekcie Next.js:
1# Dockerfile
2# Etap 1: Instalacja zależności
3FROM node:18-alpine AS deps
4
5WORKDIR /app
6
7# Kopiowanie plików package
8COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
9
10# Instalacja zależności w zależności od 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# Etap 2: Budowanie aplikacji
19FROM node:18-alpine AS builder
20
21WORKDIR /app
22
23# Kopiowanie zależności z poprzedniego etapu
24COPY /app/node_modules ./node_modules
25
26# Kopiowanie kodu źródłowego
27COPY . .
28
29# Budowanie aplikacji Next.js
30RUN npm run build
31
32# Etap 3: Środowisko produkcyjne
33FROM node:18-alpine AS runner
34
35WORKDIR /app
36
37# Dodanie użytkownika nieadministracyjnego
38RUN addgroup --system --gid 1001 nodejs
39RUN adduser --system --uid 1001 nextjs
40
41# Kopiowanie plików publicznych
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"]Podobnie jak
.gitignore, plik .dockerignore określa, które pliki mają być ignorowane podczas budowania obrazu Docker: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.coverageAby zoptymalizować rozmiar obrazu Docker, Next.js oferuje opcję
standalone output:1// next.config.js
2/** @type {import('next').NextConfig} */
3const nextConfig = {
4 output: 'standalone',
5 experimental: {
6 // Optymalizacje dla Docker
7 outputFileTracingRoot: path.join(__dirname, '../../'),
8 },
9 // Konfiguracja dla zmiennych środowiskowych
10 env: {
11 CUSTOM_KEY: process.env.CUSTOM_KEY,
12 },
13 // Optymalizacja obrazów
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# Instalacja zależności globalnych
7RUN npm install -g nodemon
8
9# Kopiowanie plików package
10COPY package*.json ./
11
12# Instalacja zależności
13RUN npm ci
14
15# Kopiowanie kodu źródłowego
16COPY . .
17
18EXPOSE 3000
19
20CMD ["npm", "run", "dev"]1# Dockerfile.prod
2# Etap budowania
3FROM node:18-alpine AS build
4
5WORKDIR /app
6
7# Instalacja zależności
8COPY package*.json ./
9RUN npm ci --only=production && npm cache clean --force
10
11# Kopiowanie i budowanie aplikacji
12COPY . .
13RUN npm run build
14
15# Etap produkcyjny
16FROM node:18-alpine AS production
17
18WORKDIR /app
19
20# Tworzenie użytkownika
21RUN addgroup -g 1001 -S nodejs
22RUN adduser -S nextjs -u 1001
23
24# Kopiowanie tylko niezbędnych plików
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# Instalacja tylko niezbędnych pakietów
5RUN apk add --no-cache libc6-compat
6
7FROM base AS deps
8WORKDIR /app
9
10# Kopiowanie plików zależności
11COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
12
13# Instalacja zależności
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# Budowanie z optymalizacjami
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# Aktualizacja pakietów systemowych
5RUN apk update && apk upgrade
6
7# Instalacja narzędzi bezpieczeństwa
8RUN apk add --no-cache dumb-init
9
10# Użycie użytkownika nieadministracyjnego
11RUN addgroup -g 1001 -S nodejs
12RUN adduser -S nextjs -u 1001
13
14FROM base AS deps
15WORKDIR /app
16
17# Kopiowanie tylko niezbędnych plików
18COPY package*.json ./
19
20# Instalacja z audytem bezpieczeństwa
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# Skanowanie bezpieczeństwa przed budowaniem
30RUN npm audit
31
32# Budowanie aplikacji
33RUN npm run build
34
35FROM base AS runner
36WORKDIR /app
37
38ENV NODE_ENV=production
39
40# Kopiowanie z odpowiednimi uprawnieniami
41COPY /app/.next/standalone ./
42COPY /app/.next/static ./.next/static
43
44# Ustawienie użytkownika
45USER nextjs
46
47# Healthcheck dla monitorowania
48HEALTHCHECK \
49 CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
50
51EXPOSE 3000
52
53# Użycie dumb-init dla lepszego zarządzania procesami
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 // Sprawdzenie połączeń z bazą danych, Redis, itp.
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 # Tutaj kod wdrażania na serwer produkcyjny1# 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# ✅ Dobre - grupowanie poleceń
2RUN apk update && \
3 apk add --no-cache git curl && \
4 rm -rf /var/cache/apk/*
5
6# ❌ Złe - osobne warstwy
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# Użycie Docker secrets zamiast zmiennych środowiskowych
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 z npm ci w Alpine Linux
1# Rozwiązanie: dodanie python i make
2RUN apk add --no-cache python3 make g++Problemy z Next.js Image Optimization
1// next.config.js
2module.exports = {
3 images: {
4 unoptimized: true, // Dla środowisk bez sharp
5 },
6}Problemy z pamięcią
1# Ograniczenie pamięci Node.js
2ENV NODE_OPTIONS="--max-old-space-size=1024"Docker z Next.js to potężne połączenie, które umożliwia:
Podobnie jak starożytni budowniczowie tworzyli trwałe struktury przy pomocy standardowych elementów, Docker pozwala nam budować niezawodne, skalowalne aplikacje Next.js, które działają jednakowo w każdym środowisku.
W następnych lekcjach poznamy zaawansowane techniki orkiestracji kontenerów oraz strategie wdrażania aplikacji Next.js w środowiskach chmurowych.