← Retour aux tutoriels

Docker en local vs en production : compromis d’architecture et de workflow

dockerdéveloppement localproductionarchitectureworkflowci/cdconteneursdevops

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 :

Cela implique souvent :

Docker en production : priorité à la robustesse et à la sécurité

En production, on veut :

Cela implique souvent :


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 :

En production, cela augmente :

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 :


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 :

Inconvénients :

Astuce : ne pas écraser node_modules

Un piège classique : monter /app écrase node_modules installé dans l’image. Solutions :

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 à :

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 :

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 :

Préférez :

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 :

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 :

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 :

C’est pratique, mais dangereux si transposé tel quel en prod.

Production : séparation des responsabilités

Bon pattern :

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 :

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 :


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

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”

Commandes utiles :

docker builder prune
docker system df

Production : optimiser taille et cold start

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 :

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 :

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”

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 :

Mais gardez une image prod séparée.

Workflow C : “Prod-first” (recommandé pour services critiques)


15) Cas pratique : API + DB, local vs prod (commandes concrètes)

Local : API en dev + Postgres

  1. Créer un réseau :
docker network create devnet
  1. 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
  1. Builder l’API en cible dev :
docker build -t monapi:dev --target dev .
  1. 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)

  1. Builder et pousser :
docker build -t registry.exemple.com/monapi:1.0.0 --target prod .
docker push registry.exemple.com/monapi:1.0.0
  1. 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
  1. 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


17) Checklist pratique

Checklist Docker local (efficacité)

Checklist Docker production (robustesse)


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.