Cours Next.js · Module 9: Intégrations et fonctions avancées

Next.js et Docker - Conteneurisation des applications

10 min de lecture
Dans cette leçon14

Tout comme les bâtisseurs antiques utilisaient des moules standard pour fabriquer des briques identiques, Docker nous permet aujourd'hui de créer des conteneurs standard et portables pour nos applications Next.js. Dans cette leçon, tu vas apprendre à « empaqueter » ton application dans un conteneur Docker qui s'exécutera de la même manière dans n'importe quel environnement - de la machine locale d'un développeur aux serveurs de production.

Qu'est-ce que Docker et pourquoi est-ce utile pour Next.js ?

Docker est une plateforme de conteneurisation d'applications qui permet d'empaqueter une application avec toutes ses dépendances dans des conteneurs légers et portables. Pour les applications Next.js, Docker offre de nombreux avantages :

Avantages de l'utilisation de Docker avec Next.js

  1. Cohérence des environnements - l'application s'exécute à l'identique sur chaque système
  2. Isolation - chaque application a son propre environnement dédié
  3. Portabilité - migration facile entre les environnements
  4. Scalabilité - mise à l'échelle horizontale simple
  5. Versioning - possibilité de gérer les versions des environnements

Les bases de Docker pour Next.js

Structure des fichiers Docker

Commençons par créer les fichiers Docker de base dans notre projet Next.js :

1# Dockerfile
2# Étape 1 : Installe les dépendances
3FROM node:18-alpine AS deps
4
5WORKDIR /app
6
7# Copie les fichiers package
8COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
9
10# Installe les dépendances selon le 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# Étape 2 : Build de l'application
19FROM node:18-alpine AS builder
20
21WORKDIR /app
22
23# Copie les dépendances de l'étape précédente
24COPY --from=deps /app/node_modules ./node_modules
25
26# Copie le code source
27COPY . .
28
29# Build de l'application Next.js
30RUN npm run build
31
32# Étape 3 : Environnement de production
33FROM node:18-alpine AS runner
34
35WORKDIR /app
36
37# Ajoute un utilisateur non-root
38RUN addgroup --system --gid 1001 nodejs
39RUN adduser --system --uid 1001 nextjs
40
41# Copie les fichiers publics
42COPY --from=builder /app/public ./public
43
44# Copie l'application compilée
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"]

Fichier .dockerignore

Comme .gitignore, le fichier .dockerignore indique quels fichiers doivent être ignorés lors du build d'une image 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

Configuration de Next.js pour Docker

Sortie Standalone

Pour optimiser la taille de l'image Docker, Next.js propose l'option de sortie standalone :

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

Docker Compose pour les environnements de développement

Configuration Docker Compose de base

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 pour l'environnement de développement

1# Dockerfile.dev
2FROM node:18-alpine
3
4WORKDIR /app
5
6# Installe les dépendances globales
7RUN npm install -g nodemon
8
9# Copie les fichiers package
10COPY package*.json ./
11
12# Installe les dépendances
13RUN npm ci
14
15# Copie le code source
16COPY . .
17
18EXPOSE 3000
19
20CMD ["npm", "run", "dev"]

Dockerfile multi-étapes pour la production

Dockerfile optimisé

1# Dockerfile.prod
2# Build stage
3FROM node:18-alpine AS build
4
5WORKDIR /app
6
7# Installe les dépendances
8COPY package*.json ./
9RUN npm ci --only=production && npm cache clean --force
10
11# Copie et build de l'application
12COPY . .
13RUN npm run build
14
15# Production stage
16FROM node:18-alpine AS production
17
18WORKDIR /app
19
20# Crée l'utilisateur
21RUN addgroup -g 1001 -S nodejs
22RUN adduser -S nextjs -u 1001
23
24# Copie uniquement les fichiers nécessaires
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"]

Gestion des variables d'environnement

Fichier d'environnement

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 avec fichiers d'environnement

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:

Optimisation des images Docker

Réduire la taille de l'image

1# Dockerfile.optimized
2FROM node:18-alpine AS base
3
4# Installe uniquement les packages requis
5RUN apk add --no-cache libc6-compat
6
7FROM base AS deps
8WORKDIR /app
9
10# Copie les fichiers de dépendances
11COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
12
13# Installe les dépendances
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 avec optimisations
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"]

Sécurité dans les conteneurs Docker

Bonnes pratiques de sécurité

1# Dockerfile.secure
2FROM node:18-alpine AS base
3
4# Met à jour les packages système
5RUN apk update && apk upgrade
6
7# Installe les outils de sécurité
8RUN apk add --no-cache dumb-init
9
10# Utilise un utilisateur non-administrateur
11RUN addgroup -g 1001 -S nodejs
12RUN adduser -S nextjs -u 1001
13
14FROM base AS deps
15WORKDIR /app
16
17# Copie uniquement les fichiers nécessaires
18COPY package*.json ./
19
20# Installe avec un audit de sécurité
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# Scan de sécurité avant le build
30RUN npm audit
31
32# Build de l'application
33RUN npm run build
34
35FROM base AS runner
36WORKDIR /app
37
38ENV NODE_ENV=production
39
40# Copie avec les permissions appropriées
41COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
42COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
43
44# Définit l'utilisateur
45USER nextjs
46
47# Healthcheck pour le 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# Utilise dumb-init pour une meilleure gestion des processus
54ENTRYPOINT ["dumb-init", "--"]
55CMD ["node", "server.js"]

Monitoring et journalisation

Configuration de la journalisation

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

Endpoint Health Check

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    // Vérifie les connexions à la base de données, 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 avec Docker

GitHub Actions avec 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

Mise à l'échelle et orchestration

Déploiement Kubernetes

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

Bonnes pratiques

1. Optimisation des couches

1# Bien - regroupe les commandes
2RUN apk update && \
3    apk add --no-cache git curl && \
4    rm -rf /var/cache/apk/*
5
6# Mauvais - couches séparées
7RUN apk update
8RUN apk add --no-cache git
9RUN apk add --no-cache curl

2. Utiliser .dockerignore

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

3. Gestion des secrets

1# Utilise les secrets Docker au lieu des variables d'environnement
2docker service create \
3  --name nextjs-app \
4  --secret db_password \
5  --secret api_key \
6  myapp/nextjs:latest

4. Monitoring et alertes

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}

Dépannage

Problèmes courants et solutions

  1. Problème avec npm ci sur Alpine Linux

    1# Solution : installe python et make
    2RUN apk add --no-cache python3 make g++
  2. Problèmes avec l'optimisation d'images de Next.js

    1// next.config.js
    2module.exports = {
    3  images: {
    4    unoptimized: true, // Pour les environnements sans sharp
    5  },
    6}
  3. Problèmes de mémoire

    1# Limite la mémoire de Node.js
    2ENV NODE_OPTIONS="--max-old-space-size=1024"

Résumé

Docker associé à Next.js est une combinaison puissante qui permet :

  • Des environnements cohérents à chaque étape du développement
  • Une mise à l'échelle facile des applications dans le cloud
  • Des déploiements sûrs avec isolation des processus
  • Une gestion efficace des dépendances et des versions

Tout comme les bâtisseurs antiques créaient des structures durables à partir de composants standardisés, Docker nous permet de construire des applications Next.js fiables et scalables qui se comportent de la même façon dans chaque environnement.

Dans les leçons suivantes, nous explorerons des techniques avancées d'orchestration de conteneurs et des stratégies de déploiement d'applications Next.js dans des environnements cloud.

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Lequel des éléments suivants n'est PAS un avantage direct de la conteneurisation de Next.js avec Docker ?

  2. 2. Quelle pratique est INCORRECTE pour le déploiement en production de Next.js dans Docker ?

Tâches pratiques dans le jeu

  • Tri vertical

    Remets dans l'ordre les étapes de conteneurisation d'une application Next.js avec Docker

  • Clique dans l'ordre

    Remets dans l'ordre les étapes de l'orchestration Docker.

  • Tri vertical

    Remets dans le bon ordre les directives de base d'un Dockerfile pour une application Next.js :

  • Éditeur de code

    Écris un Dockerfile multi-étapes pour Next.js : installe les dépendances et construis l'application dans une étape basée sur une image node, copie seulement le résultat du build dans l'étape finale (COPY --from) et démarre l'application avec CMD.

Articles utiles