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é