Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Next.js i Docker - Konteneryzacja Aplikacji

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.

Czym jest Docker i dlaczego jest przydatny dla Next.js?

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:

Zalety używania Docker z Next.js

  1. Konsystentność środowiska - aplikacja działa identycznie na każdym systemie
  2. Izolacja - każda aplikacja ma swoje dedykowane środowisko
  3. Przenośność - łatwe przenoszenie między środowiskami
  4. Skalowalność - proste skalowanie poziome
  5. Wersjonowanie - możliwość zarządzania wersjami środowisk

Podstawy Docker dla Next.js

Struktura plików Docker

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 --from=deps /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 --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

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.coverage

Konfiguracja Next.js dla Docker

Standalone Output

Aby 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 = nextConfig

Docker Compose dla środowisk deweloperskich

Podstawowa konfiguracja Docker Compose

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 dla środowiska deweloperskiego

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"]

Multi-stage Dockerfile dla produkcji

Optymalizowany Dockerfile

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 --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"]

Zarządzanie zmiennymi środowiskowymi

Plik environment

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 z plikami środowiskowymi

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:

Optymalizacja obrazów Docker

Zmniejszanie rozmiaru obrazu

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 --from=deps /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 --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"]

Bezpieczeństwo w kontenerach Docker

Dobre praktyki bezpieczeństwa

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 --from=deps /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 --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
42COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
43
44# Ustawienie użytkownika
45USER nextjs
46
47# Healthcheck dla monitorowania
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# Użycie dumb-init dla lepszego zarządzania procesami
54ENTRYPOINT ["dumb-init", "--"]
55CMD ["node", "server.js"]

Monitoring i logowanie

Konfiguracja logowania

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    // 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}

CI/CD z Docker

GitHub Actions z 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        # Tutaj kod wdrażania na serwer produkcyjny

Skalowanie i orkiestracja

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

Najlepsze praktyki

1. Optymalizacja warstw

1# ✅ 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 curl

2. Używanie .dockerignore

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

3. Zarządzanie secretami

1# 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:latest

4. Monitoring i 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

Typowe problemy i rozwiązania

  1. Problem z npm ci w Alpine Linux

    1# Rozwiązanie: dodanie python i make
    2RUN apk add --no-cache python3 make g++
  2. Problemy z Next.js Image Optimization

    1// next.config.js
    2module.exports = {
    3  images: {
    4    unoptimized: true, // Dla środowisk bez sharp
    5  },
    6}
  3. Problemy z pamięcią

    1# Ograniczenie pamięci Node.js
    2ENV NODE_OPTIONS="--max-old-space-size=1024"

Podsumowanie

Docker z Next.js to potężne połączenie, które umożliwia:

  • Konsystentne środowiska na wszystkich etapach rozwoju
  • Łatwe skalowanie aplikacji w chmurze
  • Bezpieczne wdrażanie z izolacją procesów
  • Efektywne zarządzanie zależnościami i wersjami

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.

Vai a CodeWorlds