Docker pour les devs - 22 - Conteneuriser un frontend

Builder React ou Next.js dans Docker, multi-stage avec nginx ou Caddy, Caddyfile pour SPA et build args pour les URLs d'API.

  1. 01 Docker pour les devs - 00 - Pourquoi Docker change tout
  2. 02 Docker pour les devs - 01 - Containers vs VMs
  3. 03 Docker pour les devs - 02 - L'architecture de Docker
  4. 04 Docker pour les devs - 03 - Docker Desktop, Engine et alternatives
  5. 05 Docker pour les devs - 04 - Écrire un Dockerfile
  6. 06 Docker pour les devs - 05 - Layers et cache
  7. 07 Docker pour les devs - 06 - Le .dockerignore
  8. 08 Docker pour les devs - 07 - Multi-stage builds
  9. 09 Docker pour les devs - 08 - Choisir son image de base
  10. 10 Docker pour les devs - 09 - Docker Compose, les bases
  11. 11 Docker pour les devs - 10 - Docker Compose avance
  12. 12 Docker pour les devs - 11 - Networking Docker, les bases
  13. 13 Docker pour les devs - 12 - Networking Docker avance
  14. 14 Docker pour les devs - 13 - Volumes et persistance
  15. 15 Docker pour les devs - 14 - Variables d'environnement et secrets
  16. 16 Docker pour les devs - 15 - Permissions et utilisateurs
  17. 17 Docker pour les devs - 16 - Docker et monorepo
  18. 18 Docker pour les devs - 17 - Dev vs Prod
  19. 19 Docker pour les devs - 18 - ENTRYPOINT, CMD et scripts d'initialisation
  20. 20 Docker pour les devs - 19 - Debugger ses conteneurs
  21. 21 Docker pour les devs - 20 - Bases de donnees dans Docker
  22. 22 Docker pour les devs - 21 - Sauvegardes et restauration
  23. 23 Docker pour les devs - 22 - Conteneuriser un frontend
  24. 24 Docker pour les devs - 23 - Sécurité des conteneurs
  25. 25 Docker pour les devs - 24 - Optimisation des images
  26. 26 Docker pour les devs - 25 - Builds multi-platform
  27. 27 Docker pour les devs - 26 - Limiter les ressources de tes conteneurs
  28. 28 Docker pour les devs - 27 - Gerer les logs comme un adulte
  29. 29 Docker pour les devs - 28 - Healthchecks et restart policies
  30. 30 Docker pour les devs - 29 - Nettoyer Docker avant qu'il mange ton disque
  31. 31 Docker pour les devs - 30 - Registries et stratégie de tags
  32. 32 Docker pour les devs - 31 - Docker en CI/CD
  33. 33 Docker pour les devs - 32 - Au-dela de Compose
  34. 34 Docker pour les devs - 33 - Glossaire Docker de A a Z

22 - Conteneuriser un frontend

Ce que tu vas apprendre

  • Construire une app React ou Next.js dans Docker
  • Le multi-stage build : builder avec Node, servir avec nginx ou Caddy
  • Configurer Caddy pour le routing SPA
  • Utiliser les build args pour les URLs d'API
  • Dev server dans Docker vs dev local : le vrai debat

Prerequisites

Avoir suivi l'article sur les backups. Connaitre les bases d'une app frontend (React, Vue, ou Next.js). Savoir ce qu'est un build statique.


Pendant longtemps, je ne dockerisais pas mes frontends. "C'est des fichiers statiques, je les mets sur un CDN et c'est fini." Et puis j'ai eu besoin de server-side rendering, de variables d'environnement au runtime, de headers de sécurité custom. Et la, Docker redevient pertinent.

Le Dockerfile multi-stage classique

Pour une SPA React classique qui généré des fichiers statiques :

dockerfile# Stage 1 : build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2 : servir
FROM caddy:2-alpine
COPY --from=builder /app/dist /srv
COPY Caddyfile /etc/caddy/Caddyfile
EXPOSE 80

Le premier stage installe les dépendances et fait le build. Le deuxieme stage prend juste les fichiers générés et les sert avec Caddy. L'image finale fait environ 40 Mo au lieu de 1 Go avec tout Node.js dedans.

Pourquoi Caddy plutot que nginx ? La config est plus simple, le HTTPS automatique est inclus, et pour un dev qui ne veut pas apprendre la syntaxe nginx, c'est un gain de temps énorme.

Le Caddyfile pour une SPA

Le problème des SPA : quand tu tapes /about dans le navigateur, le serveur cherche un fichier /about/index.html qui n'existe pas. Il faut renvoyer index.html pour toutes les routes.

:80 {
    root * /srv
    file_server
    try_files {path} /index.html
    encode gzip

    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy strict-origin-when-cross-origin
    }
}

Le try_files {path} /index.html fait le travail. Si le fichier existe (CSS, JS, images), il le sert. Sinon, il renvoie index.html et le router JavaScript prend le relais.

Pour la version nginx, si tu preferes :

nginxserver {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Build args pour les URLs d'API

Un problème classique : l'URL de l'API change entre les environnements. En dev c'est http://localhost:3000, en prod c'est https://api.monsite.com. Les frameworks frontend utilisent des variables d'environnement a la compilation.

dockerfileFROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

ARG VITE_API_URL=http://localhost:3000
ENV VITE_API_URL=$VITE_API_URL

RUN npm run build

FROM caddy:2-alpine
COPY --from=builder /app/dist /srv
COPY Caddyfile /etc/caddy/Caddyfile
bash# Build pour la prod
docker build --build-arg VITE_API_URL=https://api.monsite.com -t frontend:prod .

# Build pour la staging
docker build --build-arg VITE_API_URL=https://api.staging.monsite.com -t frontend:staging .

Attention : les build args sont graves dans l'image. Si tu changes l'URL d'API, tu dois rebuilder. Pour des configs vraiment dynamiques, certains injectent un fichier config.json au runtime via un volume ou un script d'entrypoint. Sur paltemps.fr, j'utilise les build args parce que je n'ai que deux environnements.

Next.js : le cas particulier

Next.js avec SSR ne généré pas que des fichiers statiques. Il a besoin de Node.js au runtime :

dockerfileFROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT=3000

CMD ["node", "server.js"]

Ce Dockerfile utilise le standalone output de Next.js (a activer dans next.config.js avec output: "standalone"). Ca copie uniquement les fichiers nécessaires, sans les node_modules complets. L'image fait environ 120 Mo au lieu de 500+.

Le user nextjs non-root, c'est une bonne pratique de sécurité qu'on detaillera dans l'article sur la sécurité.

Dev server : Docker ou local ?

La question qui divise. Faut-il lancer npm run dev dans Docker ou sur ta machine ?

Mon avis : pour le frontend, développé en local. Le hot reload est plus rapide, le filesystem watching marche mieux (surtout sur macOS avec les bind mounts), et tu n'as pas de latence ajoutee.

Docker pour le dev frontend, ca fait sens dans deux cas :

  • Tu veux que toute l'équipe ait exactement le meme environnement
  • Tu as des dépendances système spécifiques (sharp pour le traitement d'images, par exemple)

Si tu veux quand meme un setup Docker pour le dev :

yamlservices:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    ports:
      - "5173:5173"
    volumes:
      - ./frontend:/app
      - /app/node_modules
    environment:
      - WATCHPACK_POLLING=true

Le WATCHPACK_POLLING=true force le polling au lieu de inotify pour le file watching. C'est plus lent mais ca marche dans Docker. Le volume /app/node_modules empeche le bind mount d'ecraser les node_modules du conteneur.

dockerfile# Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

Le --host 0.0.0.0 est obligatoire. Sans ca, Vite écoûte sur localhost, inaccessible depuis l'extérieur du conteneur.

Résumé

  • Le multi-stage build séparé la compilation (Node) du serving (Caddy/nginx).
  • Caddy simplifie la config SPA avec try_files {path} /index.html.
  • Les build args injectent les URLs d'API au moment du build.
  • Next.js SSR a besoin de Node.js au runtime, utilise le mode standalone.
  • Pour le dev quotidien du frontend, le local est souvent plus confortable que Docker.

Article précédent : Docker 21 - Backup Article suivant : Docker 23 - Sécurité

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.