Conteneuriser votre première app avec Docker Compose
Ce guide couvre l'écriture d'un Dockerfile multi-étapes minimaliste et d'un docker-compose.yml qui relient votre application à sa base de données, sans l'image surchargée ni le cache de build cassé qu'un Dockerfile naïf produit.
Build multi-étapes : séparer ce qui compile de ce qui s'exécute
Le conteneur qui compile votre application n'a pas besoin d'être celui qui l'exécute. Donnez à l'étape « builder » tout ce dont elle a besoin : node_modules complet, dépendances de développement, caches de build. Puis ne copiez que le résultat compilé dans une image d'exécution propre.
# Étape 1 : build
FROM node:20.17.0-slim AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Étape 2 : exécution
FROM node:20.17.0-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]Seul --from=builder /app/dist se retrouve dans l'image finale, et ce seul changement fait généralement passer un service Node ou Python de 1 à 1,5 Go à 150-250 Mo.
Fixez la version de votre image de base. FROM node:20-slim paraît inoffensif, mais ce tag évolue dans le temps ; un rebuild dans six mois peut récupérer une version mineure différente et changer silencieusement le comportement de votre application. node:20.17.0-slim est reproductible ; node:20-slim est une cible mouvante.
Ordonner ses instructions pour le cache, pas pour la lisibilité
Docker met en cache chaque couche et ne l'invalide (elle et tout ce qui suit) que lorsque ses entrées changent. L'erreur la plus fréquente consiste à copier tout le projet avant d'installer les dépendances. Résultat : un npm install (ou pip install, ou bundle install) complet à chaque ligne de code source modifiée.
Copiez d'abord uniquement le manifeste des dépendances, installez, puis copiez le reste du code source :
COPY package.json package-lock.json ./
RUN npm ci
COPY . .Modifier src/routes/users.ts invalide désormais la couche COPY . . et tout ce qui la suit, mais la couche npm ci au-dessus reste en cache. Sur un runner CI, c'est la différence entre un build de 90 secondes et un build de 4 secondes.
Un .dockerignore pour garder un contexte de build honnête
Chaque docker build envoie l'intégralité du répertoire du projet au démon Docker en tant que « contexte de build », avant même de regarder votre Dockerfile. Sans .dockerignore, ce contexte inclut node_modules, .git, des fichiers .env contenant de vrais identifiants, et votre dossier dist local, le tout téléversé à chaque build.
node_modules
npm-debug.log
dist
.git
.gitignore
.env
.env.*
!.env.example
Dockerfile
.dockerignore
README.md
.vscode
coverageSans lui, un fichier .env contenant de vrais identifiants peut se retrouver figé dans une couche d'image. Les couches sont mises en cache et réutilisées, donc supprimer le fichier dans une étape RUN ultérieure ne l'efface pas de l'historique de l'image, inspectable par quiconque dispose de docker history et d'un accès au registre.
Assembler le tout avec Docker Compose
Un seul conteneur ne suffit presque jamais : la plupart des applications ont besoin d'une base de données à côté, ne serait-ce que pour le développement local. Compose permet de décrire toute la pile, avec un volume nommé pour que vos données Postgres survivent à un docker compose down.
version: "3.9"
services:
app:
build:
context: .
target: runtime
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:app@db:5432/app
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/healthz"]
interval: 10s
timeout: 3s
retries: 3
db:
image: postgres:16.4-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 3s
retries: 5
cache:
image: redis:7.4-alpine
volumes:
pgdata:Sans condition: service_healthy, depends_on attend seulement que le conteneur démarre, pas que Postgres accepte réellement des connexions, votre application va crash-looper lors de ses premières tentatives de connexion contre une base qui n'est pas encore prête.
HEALTHCHECK, utilisateur non-root et image légère
Deux habitudes séparent un conteneur qui survit en production d'un conteneur qui fonctionne simplement par chance sur votre poste.
D'abord, un HEALTHCHECK dans le Dockerfile lui-même permet à docker ps comme à votre orchestrateur de confirmer que l'application sert réellement du trafic, puisqu'un processus peut rester actif longtemps après avoir cessé de répondre :
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:3000/healthz || exit 1Ensuite, exécutez le conteneur avec un utilisateur non-root. Les images officielles node fournissent d'ailleurs un utilisateur node exactement pour cette raison :
USER nodeFaire tourner un conteneur en root n'est pas un risque théorique. Une évasion de conteneur combinée à un processus root à l'intérieur donne à un attaquant les droits root sur tout ce vers quoi il s'échappe. Cela coûte une seule ligne à éviter, et tout scanner de sécurité d'image de base signalera son absence.
Enfin, vérifiez ce que vous avez réellement livré. docker images vous donne la taille, mais docker history <image> vous montre quelle couche l'a produite. Vérifiez-le avant qu'une image n'approche un registre : c'est le moyen le plus rapide de repérer un apt-get égaré ou un dossier de cache oublié avant qu'il ne devienne 400 Mo de poids mort.
L'écart entre un Dockerfile naïf et un Dockerfile pensé pour la production tient à une poignée d'habitudes : builds multi-étapes, ordre des instructions pensé pour le cache, .dockerignore, HEALTHCHECK, et utilisateur non-root.
Envie de faire tourner ça en production ?
Ce tutoriel couvre les concepts et l'architecture. Si vous voulez l'implémenter dans votre propre infrastructure, ou monter en compétence pour posséder ce sujet durablement, je propose du mentorat individuel construit autour de votre environnement réel, pas une formation générique.
Ce tutoriel
- Concepts clés et architecture principale
- Extraits de code illustratifs
- Le raisonnement derrière chaque décision
Mentorat individuel
- Des sessions de travail sur votre propre environnement
- Des réponses directes aux cas particuliers que vous rencontrez
- Un retour sur votre implémentation réelle
- Un accompagnement continu pendant que vous la construisez