17 - Dev vs Prod
Ce que tu vas apprendre
- Le pattern compose.yaml + compose.override.yaml
- Les targets multi-stage pour dev et prod
- Le hot reload en dev avec bind mounts et watch
- Le build optimise pour la production
- Les fichiers d'environnement par contexte
- Un Makefile pour ne plus taper de commandes a rallonge
Prerequisites
- Avoir suivi l'article sur les monorepos
- Comprendre le multi-stage build (vu dans les articles précédents)
En dev, tu veux du hot reload, des logs verbeux, un debugger attache, et des outils comme Adminer pour voir ta base. En prod, tu veux une image minimale, des logs structures, zero outil superflu, et un build optimise. Le defi, c'est de gerer les deux sans dupliquer toute la configuration.
Le pattern override
On en a parle dans l'article Compose avance, mais c'est ici qu'il prend tout son sens.
Le fichier compose.yaml contient la base commune :
yamlservices:
app:
build:
context: .
dockerfile: Dockerfile
environment:
DATABASE_URL: postgres://postgres:secret@db:5432/myapp
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata:
Le fichier compose.override.yaml ajoute les spécificités dev (charge automatiquement) :
yamlservices:
app:
build:
target: development
ports:
- "3000:3000"
- "9229:9229" # debugger Node
volumes:
- ./src:/app/src
command: node --inspect=0.0.0.0:9229 --watch src/index.js
environment:
NODE_ENV: development
DEBUG: "true"
adminer:
image: adminer
ports:
- "8080:8080"
Pour la production, un fichier compose.prod.yaml :
yamlservices:
app:
build:
target: production
ports:
- "3000:3000"
environment:
NODE_ENV: production
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
db:
restart: unless-stopped
En dev, un simple docker compose up charge compose.yaml + compose.override.yaml.
En prod :
bashdocker compose -f compose.yaml -f compose.prod.yaml up -d
Multi-stage : dev vs prod dans le meme Dockerfile
Un Dockerfile avec des stages nommes pour chaque environnement :
dockerfileFROM node:20-alpine AS base
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable
# --- Development ---
FROM base AS development
RUN pnpm install
COPY . .
# Pas de CMD ici : on le met dans compose.override.yaml
CMD ["node", "--watch", "src/index.js"]
# --- Build ---
FROM base AS build
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# --- Production ---
FROM node:20-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -D appuser
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=build --chown=appuser:appgroup /app/package.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
Le stage development garde toutes les devDependencies et le code source. Le stage production ne copie que le dist et les node_modules de production. L'image finale est plus petite et plus sécurisée (voir l'article sur les permissions).
Hot reload en dev
Deux approches pour le rechargement automatique :
Bind mounts + --watch
La méthode classique. Tu montes ton code source dans le conteneur et tu lances Node avec --watch :
yaml# compose.override.yaml
services:
app:
volumes:
- ./src:/app/src
command: node --watch src/index.js
Simple et efficace. Mais les bind mounts peuvent etre lents sur Docker Desktop (Mac surtout).
Docker Compose Watch
L'alternative moderne, vue dans Compose avance :
yaml# compose.override.yaml
services:
app:
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package.json
bashdocker compose watch
Watch copie les fichiers modifies dans le conteneur sans passer par un bind mount. C'est souvent plus rapide sur Mac et Windows.
Environnement par contexte
Sur paltemps.fr, j'ai trois fichiers d'environnement :
.env # valeurs par defaut pour Compose (ports, versions)
.env.development # variables passees aux conteneurs en dev
.env.production # variables passees aux conteneurs en prod
Dans les fichiers Compose :
yaml# compose.override.yaml (dev)
services:
app:
env_file:
- .env.development
# compose.prod.yaml
services:
app:
env_file:
- .env.production
Les secrets de production ne sont jamais dans le repo. Ils viennent de Docker secrets ou d'un gestionnaire comme Vault (voir l'article sur les secrets).
Le Makefile pour simplifier
Taper docker compose -f compose.yaml -f compose.prod.yaml up -d --build a chaque fois, c'est penible. Un Makefile resout ca :
makefile.PHONY: dev prod build logs clean
dev:
docker compose up -d --build
dev-logs:
docker compose logs -f
prod:
docker compose -f compose.yaml -f compose.prod.yaml up -d --build
build:
docker compose -f compose.yaml -f compose.prod.yaml build
logs:
docker compose logs -f app
ps:
docker compose ps
clean:
docker compose down -v
restart:
docker compose restart app
shell:
docker compose exec app sh
db-shell:
docker compose exec db psql -U postgres myapp
Maintenant :
bashmake dev # lance l'environnement de dev
make prod # lance la prod
make logs # affiche les logs
make shell # ouvre un shell dans l'app
make clean # supprime tout
C'est quatre caractères au lieu de quarante. Et ca documente implicitement les commandes du projet. Un nouveau développeur ouvre le Makefile et comprend comment lancer le projet.
Exemple complet : la structure finale
project/
docker/
Dockerfile # ou a la racine
src/
index.js
compose.yaml # config commune
compose.override.yaml # dev (charge automatiquement)
compose.prod.yaml # production
.env # variables Compose
.env.development # variables dev
.env.production # variables prod (pas dans git)
.dockerignore
Makefile
package.json
Le .env.production est dans le .gitignore. Les secrets ne sont jamais commites.
La checklist dev vs prod
| Aspect | Dev | Prod |
|---|---|---|
| Stage Dockerfile | development | production |
| Source code | bind mount | copie dans l'image |
| node_modules | toutes (devDeps incluses) | production uniquement |
| Utilisateur | root (pratique) | non-root (sécurité) |
| Logs | stdout verbeux | json-file avec rotation |
| Restart policy | aucune | unless-stopped |
| Resources | illimitees | limitees (memory, CPU) |
| Outils | adminer, mailhog | rien de superflu |
Résumé
- compose.override.yaml pour le dev, compose.prod.yaml pour la prod
- Le multi-stage Dockerfile avec des targets nommes évité la duplication
- Bind mounts ou watch pour le hot reload en dev
- L'image de prod ne contient que le strict nécessaire : code compile, deps de prod, utilisateur non-root
- Un Makefile transforme des commandes de 40 caractères en 4
Navigation : Precedent : 16 - Docker et monorepo | Suivant : 18 - Init et entrypoints
Sources
- Docker Compose profiles and override par Docker
- Multi-stage builds par Docker
- Node.js Docker best practices par Node.js