Du code au conteneur : un guide Docker de production
Les conteneurs ne valent que ce que vaut la stratégie de build derrière eux. Ce guide couvre la configuration multi-stage que nous utilisons pour garder les images de production légères, l'orchestration Compose pour le développement local, et les erreurs de configuration qui gonflent les images et ralentissent les pipelines.
Containeriser une application full-stack moderne (frontend + backend + base de données) est très différent d'une configuration simple. Lorsque vous combinez des outils comme Turborepo (pour gérer le code), NestJS (pour l'API) et Python (pour l'IA), cela peut devenir profondément déroutant. Ce guide vous emmène des bases absolues de Docker jusqu'à une configuration réelle qui fonctionne vraiment.
1. Qu'est-ce que Docker ?
Pensez à Docker comme un « conteneur d'expédition » pour votre code. Il emballe votre application avec tout ce dont elle a besoin pour fonctionner — bibliothèques, paramètres, outils — pour qu'elle fonctionne exactement de la même façon sur votre ordinateur que sur un serveur.
Pourquoi l'utiliser ?
- Cohérence : Fini les excuses du genre « ça marche sur ma machine ». Si ça tourne dans Docker, ça tourne partout.
- Isolation : Vous pouvez exécuter une application Python et une application Node.js côte à côte sans qu'elles perturbent leurs configurations respectives.
- Simplicité : L'intégration d'un nouveau développeur prend des minutes, pas des jours. Il suffit de lancer
docker-compose up.
2. Conteneurs et Dockerfiles
Un conteneur est simplement la version en cours d'exécution de votre « conteneur d'expédition ». C'est une boîte légère dans laquelle vit votre application.
Un Dockerfile est le manuel d'instructions. Il indique à Docker exactement comment construire cette boîte. Voici à quoi ça ressemble :
# Utilise Node.js version 18 comme point de départ
FROM node:18
# Crée un dossier appelé /app dans le conteneur
WORKDIR /app
# Copie tous mes fichiers de projet dans ce dossier
COPY . .
# Installe les bibliothèques dont j'ai besoin
RUN npm install
# Lance l'application !
CMD ["node", "index.js"]
3. Builds multi-étapes
Quand vous construisez une maison, vous avez besoin de grues, d'échafaudages et de bétonnières. Mais une fois la maison terminée, vous enlevez tout cet équipement lourd. Vous ne laissez pas une grue dans le salon !
Les builds Docker traditionnels laissent souvent les « grues » (compilateurs, outils de build) dans l'image finale, la rendant énorme et lente. Les builds multi-étapes nous permettent de jeter les outils une fois la construction terminée.
- Étape 1 (Le filtre) : Nous sélectionnons uniquement les fichiers dont notre application spécifique a besoin (comme les ingrédients pour une recette).
- Étape 2 (L'installateur) : Nous téléchargeons toutes les bibliothèques et outils nécessaires (on prépare les casseroles et poêles).
- Étape 3 (Le constructeur) : Nous compilons le code (on prépare le repas). Cela utilise des outils lourds et fait beaucoup de désordre.
- Étape 4 (L'exécuteur) : Nous servons le repas terminé dans une assiette propre. Nous laissons les casseroles sales (outils de build) derrière nous.
Cela fait démarrer votre application plus vite et la rend bien plus sécurisée. Parce que l'image finale est plus petite, elle se télécharge plus vite, démarre plus vite, et utilise moins de mémoire.
Impact concret :
- Build traditionnel : ~1,2 Go
- Build multi-étapes : ~180 Mo
- Économie : 85% plus léger
4. Exemple concret : le service NestJS
Regardons notre API Gateway. C'est une application Node.js qui utilise Prisma pour la base de données. Nous devons la builder aussi efficacement que possible.
Le défi : le code partagé
Dans un grand projet (Monorepo), votre API peut utiliser du code provenant d'un dossier partagé. Vous ne pouvez pas simplement copier le dossier API ; vous avez besoin des éléments partagés aussi. Mais si vous copiez tout, Docker se perd et reconstruit constamment, ce qui est lent.
La solution : Turbo Prune
Nous utilisons un outil appelé turbo prune. C'est comme un filtre intelligent — il sélectionne uniquement les fichiers dont votre API spécifique a besoin et ignore le reste.
Le Dockerfile NestJS (expliqué)
# Utilise Alpine Linux pour une image de base plus légère (150 Mo vs 1 Go)
FROM node:18-alpine AS base
# === ÉTAPE 1/4 : Élagueur ===
# Objectif : Filtrer le projet pour ne garder que ce dont on a besoin
FROM base AS pruner
RUN apk add --no-cache libc6-compat
WORKDIR /app
RUN npm install -g turbo
COPY . .
# "Élaguer" le projet pour n'inclure que les fichiers de 'auth-service'
RUN turbo prune --scope=auth-service --docker
# === ÉTAPE 2/4 : Installateur ===
# Objectif : Installer les bibliothèques et préparer l'outil de base de données (Prisma)
FROM base AS installer
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Copie les fichiers filtrés depuis l'étape Élagueur
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/turbo.json ./turbo.json
COPY --from=pruner /app/apps/auth-service/prisma ./prisma
# Installe les dépendances rapidement
RUN npm install
# Génère le client Prisma (assistant base de données)
RUN npx prisma generate
# === ÉTAPE 3/4 : Constructeur ===
# Objectif : Compiler le code TypeScript en JavaScript
FROM base AS builder
WORKDIR /app
COPY --from=installer /app/ .
COPY --from=pruner /app/out/full/ .
RUN npx turbo run build --filter=auth-service...
# === ÉTAPE 4/4 : Exécuteur ===
# Objectif : L'image de production finale, légère
FROM base AS runner
WORKDIR /app
RUN apk add openssl
# Sécurité : Ne pas s'exécuter en tant que "root" (Administrateur), utiliser un utilisateur restreint
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nestjs
USER nestjs
# Copie UNIQUEMENT l'application compilée et terminée
COPY --from=builder --chown=nestjs:nestjs /app/ .
EXPOSE 3000
CMD [ "node", "apps/auth-service/dist/main.js" ]
5. Exemple concret : le service Python
Notre service IA est écrit en Python. Les applications Python ont souvent besoin d'outils système lourds (comme des compilateurs C) pour installer les bibliothèques, mais nous ne voulons pas ces outils dans notre application finale.
Le Dockerfile Python (pipeline en 4 étapes)
Nous utilisons la même logique en 4 étapes ici : Base -> Installateur -> Constructeur -> Exécuteur.
# === ÉTAPE 1/4 : Base ===
# Utilise Python 3.11 comme point de départ commun
FROM python:3.11-slim AS base
# Installe les outils lourds (gcc) nécessaires pour certaines bibliothèques
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# === ÉTAPE 2/4 : Installateur ===
# Objectif : Télécharger et installer les bibliothèques Python
FROM base AS installer
WORKDIR /app
COPY apps/ai-service/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# === ÉTAPE 3/4 : Constructeur ===
# Objectif : Préparer les fichiers finaux
FROM base AS builder
WORKDIR /app
COPY . .
# Copie les bibliothèques installées depuis l'Installateur
COPY --from=installer /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=installer /usr/local/bin /usr/local/bin
WORKDIR /app/apps/ai-service
# === ÉTAPE 4/4 : Exécuteur ===
# Objectif : L'image finale propre
FROM base AS runner
WORKDIR /app
# Copie tout depuis le Constructeur
COPY --from=builder /app/ .
# Copie les bibliothèques à nouveau pour s'assurer qu'elles sont présentes
COPY --from=installer /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=installer /usr/local/bin /usr/local/bin
RUN chmod +x apps/ai-service/start_script.sh
WORKDIR /app/apps/ai-service
EXPOSE 8000
# Lance le script de démarrage
CMD ["./start_script.sh"]
6. Tout faire tourner ensemble (orchestration)
Pour exécuter ces conteneurs ensemble, nous utilisons Docker Compose. Nous découpons notre configuration en couches pour gérer nos 8+ microservices (Auth, Paiements, IA, Analytiques, etc.) sans nous répéter.
La couche de base (docker-compose.yml)
Cela définit l'« Architecture » : quels services existent et comment ils se connectent. Nous retirons tout le « bruit du développement » (comme les montages de volumes) de cette couche.
version: '3.8'
services:
# L'API Gateway centrale
api:
build:
context: .
dockerfile: ./apps/api/Dockerfile
ports:
- "3001:3001"
depends_on:
- rabbitmq
# Microservices
auth-service:
build:
context: .
dockerfile: ./apps/auth-service/Dockerfile
depends_on:
- database
- rabbitmq
ai-service:
build:
context: .
dockerfile: ./apps/ai-service/Dockerfile
ports:
- "8000:8000"
payment-service:
build:
context: .
dockerfile: ./apps/payment/Dockerfile
depends_on:
- database
analytics-service:
build:
context: .
dockerfile: ./apps/analytics/Dockerfile
# Infrastructure
database:
image: postgres:15
environment:
- POSTGRES_DB=auth,analytics,payment,ai
volumes:
- database_volume:/var/lib/postgresql/data
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
volumes:
database_volume:
Conclusion
Construire des images Docker prêtes pour la production ne consiste pas seulement à faire tourner votre application dans un conteneur — c'est le faire correctement. Tout au long de ce guide, nous sommes passés des fondamentaux de ce qu'est Docker à l'implémentation de builds multi-étapes sophistiqués pour des applications Node.js et Python dans une architecture monorepo.
Les patterns que nous avons couverts ne sont pas des exercices théoriques. Ce sont des solutions éprouvées qui résolvent des problèmes réels :
- Les builds multi-étapes ont transformé nos images de lourds artefacts de 1,2 Go en conteneurs légers de 180 Mo — 85% plus petits, plus rapides à déployer, et plus sécurisés.
- Le prune de Turborepo a résolu le cauchemar de l'invalidation du cache en monorepo, nous permettant de ne builder que ce qui a changé.
- La séparation correcte des étapes (Élagueur → Installateur → Constructeur → Exécuteur) nous a donné clarté et maintenabilité à mesure que notre architecture a évolué vers 8+ microservices.
La vraie puissance de Docker émerge quand vous combinez ces techniques. Votre API NestJS, votre service IA Python, votre base de données PostgreSQL et votre broker de messages RabbitMQ, tous orchestrés ensemble, chacun dans son propre conteneur optimisé, communiquant de manière transparente via un réseau partagé. C'est le rêve des microservices polyglotes réalisé.
Commencez par un service. Appliquez les builds multi-étapes. Ajoutez-en un autre. Avant de vous en rendre compte, vous aurez une infrastructure prête pour la production qui est reproductible, évolutive et maintenable. L'investissement dans l'apprentissage de ces patterns porte ses fruits à chaque déploiement.
Écrit par

Tech Lead et Ingénieur Full Stack pilotant une équipe de 5 ingénieurs chez Fygurs (Paris, Remote) sur un SaaS cloud-native Azure. Diplômé de 1337 Coding School (42 Network / UM6P). Écrit sur l'architecture, l'infrastructure cloud et le leadership technique.