Docker en local vs en production : compromis d’architecture et de workflow
Docker est souvent présenté comme une réponse simple à un problème complexe : “ça marche sur ma machine”. En pratique, Docker en local et Docker en production répondent à des objectifs différents, avec des contraintes différentes. Les confondre mène à des images trop lourdes, des builds lents, des failles de sécurité, des volumes incohérents, des performances médiocres, ou des déploiements fragiles.
Ce tutoriel propose une vue architecturale et opérationnelle des compromis à faire, avec des commandes réelles et des patterns concrets. L’objectif : vous permettre de concevoir un workflow Docker cohérent du poste de dev jusqu’au cluster (ou serveur) de prod.
1) Deux contextes, deux objectifs
Docker en local : priorité au feedback rapide
En local, on veut :
- Itérer vite (hot reload, logs accessibles, rebuilds rapides)
- Déboguer (shell dans le conteneur, outils additionnels, stack “observable”)
- Simuler l’environnement sans forcément le reproduire à l’identique
- Travailler en équipe avec un onboarding simple (
docker compose up)
Cela implique souvent :
- montage de code via volumes (bind mounts),
- images “dev” avec outils (curl, vim, netcat, debuggers),
- ports exposés,
- variables d’environnement permissives,
- dépendances “mockées” ou allégées (ex : base locale, mailcatcher).
Docker en production : priorité à la robustesse et à la sécurité
En production, on veut :
- Reproductibilité (build immuable, images versionnées)
- Sécurité (surface d’attaque minimale, non-root, secrets gérés correctement)
- Performance (images petites, démarrage rapide, caching optimal)
- Observabilité (logs structurés, métriques, healthchecks)
- Scalabilité (stateless, configuration externalisée)
- Opérabilité (rollbacks, migrations maîtrisées)
Cela implique souvent :
- pas de bind mount de code,
- images multi-stage, runtime minimal,
- pas de shell/outils superflus,
- secrets via mécanismes dédiés (Swarm/K8s/Vault/SSM),
- healthchecks, limites CPU/mémoire, readiness/liveness (selon orchestrateur),
- stratégie de logs (stdout/stderr + agrégation).
2) Images : “dev” vs “prod” (et pourquoi les séparer)
Anti-pattern courant : une seule image pour tout
Une image unique “qui fait tout” finit par contenir :
- dépendances de build + runtime,
- outils de debug,
- certificats ou clés “par commodité”,
- scripts de dev,
- configuration spécifique.
En production, cela augmente :
- la taille (pull plus lent),
- la surface d’attaque,
- le risque de divergence (ex : dépendances installées différemment).
Pattern recommandé : multi-stage + cibles distinctes
Avec BuildKit, on peut construire plusieurs cibles : dev, prod, test.
Exemple Node.js (Dockerfile) :
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS base
WORKDIR /app
ENV NODE_ENV=production
FROM base AS deps
# Dépendances (cache-friendly)
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
FROM base AS dev
ENV NODE_ENV=development
# Outils de dev éventuels (à éviter en prod)
RUN apk add --no-cache bash curl
COPY --from=deps /app/node_modules /app/node_modules
COPY . .
EXPOSE 3000
CMD ["npm","run","dev"]
FROM base AS build
COPY --from=deps /app/node_modules /app/node_modules
COPY . .
RUN npm run build
FROM base AS prod
# Runtime minimal : uniquement ce qui est nécessaire
COPY --from=build /app/dist /app/dist
COPY --from=deps /app/node_modules /app/node_modules
# Optionnel : user non-root
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 3000
CMD ["node","dist/server.js"]
Build des cibles :
docker build -t monapp:dev --target dev .
docker build -t monapp:prod --target prod .
Points clés :
depsisole l’installation des dépendances pour maximiser le cache.devcontient des outils et unCMDorienté hot reload.prodest minimal et non-root.
3) Volumes et système de fichiers : vitesse vs fidélité
Local : bind mounts pour itérer
En local, on monte le code :
docker run --rm -it \
-v "$PWD":/app \
-w /app \
-p 3000:3000 \
monapp:dev
Avantages :
- modifications instantanées,
- pas besoin de rebuild à chaque changement.
Inconvénients :
- performances variables (notamment sur macOS/Windows),
- comportements différents (permissions, inotify, chemins),
- risque d’écraser des fichiers générés dans l’image.
Astuce : ne pas écraser node_modules
Un piège classique : monter /app écrase node_modules installé dans l’image. Solutions :
- monter seulement le code, pas tout
/app, - ou utiliser un volume dédié pour
node_modules.
Avec Compose (exemple conceptuel via CLI uniquement) :
docker volume create monapp_node_modules
Puis :
docker run --rm -it \
-v "$PWD":/app \
-v monapp_node_modules:/app/node_modules \
-w /app \
-p 3000:3000 \
monapp:dev
Production : pas de bind mounts de code
En production, le code doit être dans l’image. Les volumes servent surtout à :
- données persistantes (DB, uploads si vous ne pouvez pas externaliser),
- caches (selon stratégie),
- certificats gérés par la plateforme (rarement en volume manuel aujourd’hui).
Commande typique :
docker run -d \
--name monapp \
-p 3000:3000 \
--restart unless-stopped \
monapp:prod
4) Réseau : “ça parle” en local, “ça isole” en prod
Local : réseau simple, découverte de services
En local, on veut facilement connecter app, db, redis, etc. Un réseau Docker user-defined facilite la résolution DNS par nom de conteneur.
docker network create devnet
docker run -d --name db --network devnet -e POSTGRES_PASSWORD=pass postgres:16
docker run -d --name app --network devnet -p 3000:3000 monapp:dev
Dans l’app, l’hôte DB devient db:5432.
Production : segmentation, règles, exposition minimale
En production :
- on expose uniquement ce qui doit l’être (souvent via reverse proxy),
- on évite de publier des ports internes (DB non exposée),
- on utilise des réseaux séparés (front/back) si nécessaire.
Exemple avec deux réseaux :
docker network create frontnet
docker network create backnet
docker run -d --name db --network backnet -e POSTGRES_PASSWORD=pass postgres:16
docker run -d --name app --network backnet monapp:prod
docker run -d --name nginx --network frontnet -p 80:80 nginx:alpine
Puis on connecte nginx au réseau backnet si besoin d’atteindre app :
docker network connect backnet nginx
5) Variables d’environnement et configuration : 12-factor, mais avec nuance
Local : configuration permissive, mais traçable
En local, on utilise souvent un .env (attention à ne pas le committer). Exemple :
export DATABASE_URL="postgres://postgres:pass@db:5432/app"
export LOG_LEVEL="debug"
docker run --rm -it --network devnet -e DATABASE_URL -e LOG_LEVEL monapp:dev
Production : secrets et config externalisés
En production, évitez :
- secrets dans
ENVdu Dockerfile, - secrets dans l’image,
- secrets passés en clair dans l’historique du shell.
Préférez :
- mécanisme de secrets de l’orchestrateur,
- variables injectées par la plateforme,
- fichiers montés en mémoire (tmpfs) si possible.
Avec Docker “simple”, vous pouvez au minimum passer un fichier env (toujours sensible) :
docker run -d --env-file /etc/monapp/prod.env monapp:prod
Mais idéalement, utilisez un gestionnaire de secrets. Si vous êtes sur Kubernetes, utilisez Secret + envFrom ou volume (selon politique). Sur AWS/GCP/Azure, utilisez les services managés.
6) Build : vitesse en local, déterminisme en prod
Local : itération et cache
Conseils :
- activer BuildKit,
- structurer le Dockerfile pour maximiser le cache (copier
package*.jsonavant le code), - utiliser des caches de dépendances.
Activer BuildKit :
export DOCKER_BUILDKIT=1
docker build -t monapp:dev --target dev .
Production : builds reproductibles, tags immuables
En production, un tag latest est un piège. Préférez :
- tag par version (
1.4.2), - tag par commit (
gitsha-...), - digest (
@sha256:...) pour l’immuabilité.
Exemple :
docker build -t registry.exemple.com/monapp:1.4.2 --target prod .
docker push registry.exemple.com/monapp:1.4.2
Déployer une image par digest :
docker pull registry.exemple.com/monapp@sha256:0123abcd...
docker run -d registry.exemple.com/monapp@sha256:0123abcd...
7) Démarrage : scripts “magiques” vs entrypoints contrôlés
Local : scripts de confort
En local, on lance parfois :
- migrations automatiques,
- seed de DB,
- watchers.
C’est pratique, mais dangereux si transposé tel quel en prod.
Production : séparation des responsabilités
Bon pattern :
- un conteneur “app” démarre vite et sert du trafic,
- les migrations sont une étape explicite (job/commande),
- les tâches cron sont séparées (worker/cron container).
Exécuter une migration manuellement :
docker exec -it monapp node dist/migrate.js
Ou lancer un conteneur éphémère :
docker run --rm --network backnet \
-e DATABASE_URL="postgres://postgres:pass@db:5432/app" \
registry.exemple.com/monapp:1.4.2 \
node dist/migrate.js
8) Logs : lisibles en local, exploitables en production
Local : logs verbeux, format libre
En local, des logs colorés et multi-lignes sont acceptables.
Production : stdout/stderr + format structuré
En production :
- écrivez sur
stdout/stderr, - évitez d’écrire dans des fichiers internes au conteneur,
- privilégiez JSON (ou un format stable) pour l’agrégation.
Voir les logs :
docker logs -f monapp
Limiter la taille des logs côté Docker (daemon) dépend de la config du moteur, mais côté conteneur, gardez des logs raisonnables.
9) Santé applicative : “ça répond” vs “c’est prêt”
Local : simple endpoint
Un endpoint /health suffit souvent.
Production : healthcheck + readiness
Docker propose HEALTHCHECK pour indiquer si le conteneur est sain (utile même sans orchestrateur avancé).
Exemple Dockerfile :
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD wget -qO- http://127.0.0.1:3000/health || exit 1
Inspecter l’état :
docker inspect --format='{{json .State.Health}}' monapp | jq
En orchestrateur (Kubernetes), on distinguera :
- liveness : redémarrer si bloqué,
- readiness : retirer du trafic tant que pas prêt.
10) Sécurité : tolérance en local, discipline en production
10.1 Exécuter en non-root
En local, on s’en fiche souvent. En production, c’est une mesure de base.
Vérifier l’utilisateur :
docker exec -it monapp id
Dans le Dockerfile, créez un user et passez USER app.
10.2 Réduire la surface d’attaque
- images de base minimalistes (alpine, distroless, slim) selon compatibilité,
- supprimer outils inutiles,
- ne pas inclure de compilateurs en runtime (multi-stage),
- scanner les images.
Scanner (exemple avec Trivy si installé) :
trivy image monapp:prod
10.3 Capacités Linux et filesystem en lecture seule
En production, vous pouvez durcir :
docker run -d \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges \
-p 3000:3000 \
monapp:prod
Si l’app doit écrire (tmp, cache), montez un tmpfs :
docker run -d \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
-p 3000:3000 \
monapp:prod
11) Performance : vitesse de rebuild vs vitesse de démarrage
Local : optimiser le cycle “edit → run”
- cache des dépendances,
- hot reload,
- éviter les rebuilds complets.
Commandes utiles :
docker builder prune
docker system df
Production : optimiser taille et cold start
- multi-stage,
- supprimer caches,
- limiter les couches,
- choisir une base adaptée.
Comparer la taille :
docker images | grep monapp
Analyser l’historique :
docker history monapp:prod
12) Gestion des données : DB locale jetable vs persistence prod
Local : DB éphémère ou persistée pour confort
Éphémère :
docker run --rm -it -e POSTGRES_PASSWORD=pass postgres:16
Persistée via volume :
docker volume create pgdata
docker run -d --name db \
-e POSTGRES_PASSWORD=pass \
-v pgdata:/var/lib/postgresql/data \
postgres:16
Production : stratégie claire (backup, migration, HA)
En production, la DB est souvent managée (RDS/Cloud SQL) ou sur un cluster dédié. Si vous la mettez en conteneur sur un serveur, vous devez gérer :
- sauvegardes,
- restauration,
- upgrades,
- monitoring disque,
- réplication éventuelle.
Docker seul ne “résout” pas ces sujets.
13) Orchestration : Docker “simple” vs Compose vs Swarm/Kubernetes
Local : Compose comme outil d’assemblage
Même si ce tutoriel n’impose pas Compose, en pratique c’est l’outil standard pour local. L’idée : décrire plusieurs services et les lancer ensemble.
Commandes typiques :
docker compose up -d --build
docker compose logs -f
docker compose exec app sh
docker compose down -v
Production : orchestrateur ou plateforme
En production, vous avez souvent :
- Kubernetes,
- ECS, Nomad,
- Docker Swarm (moins courant),
- une PaaS (Render, Fly, Heroku-like).
Les besoins prod (autoscaling, rolling updates, secrets, policies) dépassent vite docker run.
14) Stratégies de workflow : du laptop au déploiement
Workflow A : “Image unique prod, expérience dev via cible dev”
- Dockerfile multi-stage
--target deven local--target proden CI- mêmes dépendances, mais runtime différent
Commandes :
# Local
docker build -t monapp:dev --target dev .
docker run --rm -it -p 3000:3000 -v "$PWD":/app monapp:dev
# CI/Prod
docker build -t registry.exemple.com/monapp:gitsha-$(git rev-parse --short HEAD) --target prod .
docker push registry.exemple.com/monapp:gitsha-$(git rev-parse --short HEAD)
Workflow B : “Devcontainers / environnements reproductibles”
Si vous utilisez VS Code Dev Containers ou équivalent, vous pouvez standardiser :
- version de Node/Python/Go,
- outils,
- extensions,
- scripts.
Mais gardez une image prod séparée.
Workflow C : “Prod-first” (recommandé pour services critiques)
- l’image prod est la référence,
- le local s’adapte (hot reload à côté, ou
docker runavec un mode debug), - tests et migrations sont explicitement orchestrés.
15) Cas pratique : API + DB, local vs prod (commandes concrètes)
Local : API en dev + Postgres
- Créer un réseau :
docker network create devnet
- Lancer Postgres :
docker volume create pgdata-dev
docker run -d --name db --network devnet \
-e POSTGRES_PASSWORD=pass \
-e POSTGRES_DB=app \
-v pgdata-dev:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
- Builder l’API en cible dev :
docker build -t monapi:dev --target dev .
- Lancer l’API avec bind mount :
docker run --rm -it --name api --network devnet \
-e DATABASE_URL="postgres://postgres:pass@db:5432/app" \
-e LOG_LEVEL=debug \
-p 3000:3000 \
-v "$PWD":/app \
monapi:dev
Production : API en prod + DB managée (exemple)
- Builder et pousser :
docker build -t registry.exemple.com/monapi:1.0.0 --target prod .
docker push registry.exemple.com/monapi:1.0.0
- Lancer sur serveur (DB externe) :
docker run -d --name api \
--restart unless-stopped \
-e DATABASE_URL="postgres://user:***@db-prod.exemple.com:5432/app" \
-e LOG_LEVEL=info \
-p 3000:3000 \
registry.exemple.com/monapi:1.0.0
- Vérifier santé et logs :
docker ps
docker logs -f api
docker inspect --format='{{.State.Status}} {{.State.Health.Status}}' api
16) Compromis majeurs (résumé opérationnel)
Ce qui change typiquement entre local et prod
- Code : monté en local, intégré à l’image en prod
- Dépendances : plus d’outils en local, runtime minimal en prod
- Config :
.envlocal, secrets gérés proprement en prod - Ports : exposés largement en local, minimisés en prod
- Données : DB locale jetable/persistée, DB prod managée/HA + backups
- Observabilité : logs verbeux local, logs structurés + healthchecks en prod
- Sécurité : permissif local, durci en prod (non-root, read-only, cap-drop)
- Déploiement :
docker run/compose local, CI/CD + orchestrateur en prod
17) Checklist pratique
Checklist Docker local (efficacité)
- Bind mount du code (sans casser
node_modules/venv) - Hot reload activé
- Logs verbeux et accessibles
- Services dépendants démarrables en une commande
- Possibilité de
execdans les conteneurs - Rebuilds rapides (cache BuildKit)
Checklist Docker production (robustesse)
- Image multi-stage, petite, sans outils inutiles
- Exécution non-root
- Secrets hors image, hors repo
- Healthcheck (et readiness si orchestrateur)
- Logs sur stdout/stderr, format stable
- Tags immuables (version/commit/digest)
- Ressources et durcissement (read-only, cap-drop) si possible
- Migrations gérées explicitement
- Stratégie de rollback
Conclusion
Docker en local et Docker en production ne sont pas “la même chose à une échelle différente” : ce sont deux environnements avec des priorités opposées. Le local privilégie la vitesse et le confort; la production privilégie la stabilité, la sécurité et l’opérabilité. La bonne approche consiste à assumer la divergence contrôlée : mêmes principes (conteneurisation, dépendances maîtrisées), mais images, configurations et pratiques adaptées.
Si vous voulez, décrivez votre stack (langage, framework, DB, cible de déploiement : VM, Kubernetes, PaaS) et je peux proposer un Dockerfile multi-stage et un workflow de build/déploiement adaptés, avec des commandes exactes.