Docker : Du développement à la production

Guide complet Docker pour conteneuriser vos applications. Dockerfile, Docker Compose, multi-stage builds et déploiement production expliqués avec exemples pratiques.

Guide Docker du développement à la production

Docker révolutionne la façon de développer, tester et déployer des applications. En encapsulant une application et ses dépendances dans un conteneur portable, Docker élimine le fameux "ça marche sur ma machine" et garantit une cohérence entre tous les environnements. Ce guide couvre le parcours complet, du premier Dockerfile au déploiement en production.

Docker en 2026

Docker Desktop 5.x apporte des améliorations majeures de performance, notamment le support natif de containerd, une gestion optimisée des ressources et une intégration transparente avec Kubernetes. Les images multi-architecture (ARM/x86) sont désormais standard.

Les fondamentaux de la conteneurisation

Un conteneur est une unité logicielle légère qui package le code, le runtime, les bibliothèques système et les paramètres. Contrairement aux machines virtuelles qui virtualisent le matériel, les conteneurs partagent le noyau du système hôte, ce qui les rend plus rapides à démarrer et moins gourmands en ressources.

bash
# terminal
# Installation de Docker sur Ubuntu
sudo apt update
sudo apt install -y docker.io

# Ajout de l'utilisateur au groupe docker (évite sudo)
sudo usermod -aG docker $USER

# Vérification de l'installation
docker --version
# Docker version 26.1.0, build 1234567

# Premier conteneur : télécharge l'image et exécute
docker run hello-world

Cette commande télécharge l'image hello-world depuis Docker Hub et lance un conteneur qui affiche un message de confirmation.

bash
# terminal
# Lister les conteneurs en cours d'exécution
docker ps

# Lister tous les conteneurs (y compris arrêtés)
docker ps -a

# Lister les images téléchargées
docker images

# Supprimer un conteneur
docker rm <container_id>

# Supprimer une image
docker rmi <image_name>

Ces commandes de base permettent de gérer le cycle de vie des conteneurs et des images.

Création du premier Dockerfile

Un Dockerfile contient les instructions pour construire une image Docker. Chaque instruction crée une couche (layer) dans l'image finale, ce qui permet la mise en cache et la réutilisation.

dockerfile
# Dockerfile
# Image de base : Node.js 22 sur Alpine Linux (légère)
FROM node:22-alpine

# Définition du répertoire de travail dans le conteneur
WORKDIR /app

# Copie des fichiers de dépendances en premier (optimisation cache)
COPY package*.json ./

# Installation des dépendances
RUN npm ci --only=production

# Copie du code source
COPY . .

# Exposition du port (documentation)
EXPOSE 3000

# Commande de démarrage
CMD ["node", "server.js"]

L'ordre des instructions est crucial pour l'optimisation du cache. Les fichiers qui changent rarement (package.json) sont copiés avant le code source.

bash
# terminal
# Construction de l'image avec un tag
docker build -t my-app:1.0 .

# Exécution du conteneur
docker run -d -p 3000:3000 --name my-app-container my-app:1.0

# Vérification des logs
docker logs my-app-container

# Accès au shell du conteneur
docker exec -it my-app-container sh

Le flag -d lance le conteneur en arrière-plan, -p mappe le port 3000 du conteneur vers le port 3000 de l'hôte.

Alpine vs Debian

Les images Alpine sont significativement plus petites (environ 5 Mo vs 120 Mo pour Debian). Cependant, elles utilisent musl libc au lieu de glibc, ce qui peut causer des incompatibilités avec certaines dépendances natives. En cas de problème, préférer les images basées sur Debian (node:22-slim).

Multi-stage builds pour la production

Les multi-stage builds permettent de créer des images de production optimisées en séparant l'environnement de build de l'environnement d'exécution. Seuls les artefacts nécessaires sont inclus dans l'image finale.

dockerfile
# Dockerfile.production
# ============================================
# Stage 1 : Build
# ============================================
FROM node:22-alpine AS builder

WORKDIR /app

# Copie et installation des dépendances (incluant devDependencies)
COPY package*.json ./
RUN npm ci

# Copie du code source
COPY . .

# Build de l'application (TypeScript, bundling, etc.)
RUN npm run build

# ============================================
# Stage 2 : Production
# ============================================
FROM node:22-alpine AS production

# Utilisateur non-root pour la sécurité
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copie uniquement les fichiers nécessaires depuis le stage builder
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

# Changement vers l'utilisateur non-root
USER nodejs

# Variables d'environnement
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

# Commande de démarrage
CMD ["node", "dist/server.js"]

Cette approche réduit considérablement la taille de l'image finale en excluant les outils de build, les devDependencies et les fichiers source.

bash
# terminal
# Construction avec le fichier spécifique
docker build -f Dockerfile.production -t my-app:production .

# Comparaison des tailles d'images
docker images | grep my-app
# my-app    production    abc123    150MB
# my-app    1.0           def456    450MB

La réduction de taille peut atteindre 60-70% selon les projets, améliorant les temps de déploiement et réduisant la surface d'attaque.

Docker Compose pour l'orchestration locale

Docker Compose simplifie la gestion d'applications multi-conteneurs. Un fichier YAML déclare tous les services, leurs configurations et leurs dépendances.

yaml
# docker-compose.yml
version: "3.9"

services:
  # Application principale
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    volumes:
      # Montage du code source pour le hot-reload
      - ./src:/app/src
      - ./package.json:/app/package.json
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks:
      - app-network

  # Base de données PostgreSQL
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      # Persistance des données
      - postgres_data:/var/lib/postgresql/data
      # Script d'initialisation
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  # Cache Redis
  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    networks:
      - app-network

# Volumes nommés pour la persistance
volumes:
  postgres_data:
  redis_data:

# Réseau dédié pour l'isolation
networks:
  app-network:
    driver: bridge

Les services communiquent via leurs noms (db, cache) grâce au réseau Docker interne. Les healthchecks garantissent que les dépendances sont prêtes avant le démarrage de l'application.

bash
# terminal
# Démarrage de tous les services
docker compose up -d

# Voir les logs de tous les services
docker compose logs -f

# Logs d'un service spécifique
docker compose logs -f app

# Arrêt et suppression des conteneurs
docker compose down

# Suppression incluant les volumes (attention : perte de données)
docker compose down -v

# Reconstruction après modification du Dockerfile
docker compose up -d --build

Prêt à réussir tes entretiens DevOps ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Gestion des secrets et variables d'environnement

La gestion sécurisée des secrets est cruciale en production. Docker propose plusieurs approches selon le contexte.

yaml
# docker-compose.override.yml (développement uniquement)
version: "3.9"

services:
  app:
    env_file:
      - .env.development
    environment:
      - DEBUG=true

Pour la production, les secrets Docker offrent une meilleure sécurité.

yaml
# docker-compose.production.yml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.production
    secrets:
      - db_password
      - api_key
    environment:
      - NODE_ENV=production
      - DATABASE_PASSWORD_FILE=/run/secrets/db_password
      - API_KEY_FILE=/run/secrets/api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

Le code de l'application lit les secrets depuis les fichiers montés.

config/secrets.jsjavascript
const fs = require('fs');
const path = require('path');

// Fonction utilitaire pour lire les secrets Docker
function readSecret(secretName) {
  const secretPath = `/run/secrets/${secretName}`;

  // Vérification de l'existence du fichier secret
  if (fs.existsSync(secretPath)) {
    return fs.readFileSync(secretPath, 'utf8').trim();
  }

  // Fallback vers les variables d'environnement classiques
  const envVar = secretName.toUpperCase();
  return process.env[envVar];
}

module.exports = {
  databasePassword: readSecret('db_password'),
  apiKey: readSecret('api_key'),
};

Cette approche évite d'exposer les secrets dans les variables d'environnement ou les images Docker.

Optimisation des images Docker

Plusieurs techniques permettent de réduire la taille des images et d'améliorer les performances.

dockerfile
# Dockerfile.optimized
FROM node:22-alpine AS base

# Installation des outils nécessaires en une seule couche
RUN apk add --no-cache \
    dumb-init \
    && rm -rf /var/cache/apk/*

# ============================================
# Stage : Dependencies
# ============================================
FROM base AS deps

WORKDIR /app

# Copie uniquement les fichiers de lock pour le cache
COPY package.json package-lock.json ./

# Installation avec cache npm monté (BuildKit)
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# ============================================
# Stage : Builder
# ============================================
FROM base AS builder

WORKDIR /app

COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .
RUN npm run build

# ============================================
# Stage : Production
# ============================================
FROM base AS production

# Métadonnées de l'image
LABEL maintainer="team@example.com"
LABEL version="1.0"
LABEL description="Production-ready Node.js application"

# Utilisateur non-root
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copie des dépendances de production
COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules

# Copie du build
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs

ENV NODE_ENV=production

# dumb-init comme PID 1 pour la gestion des signaux
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

L'utilisation de dumb-init assure une gestion correcte des signaux Unix, permettant un arrêt gracieux du conteneur.

bash
# terminal
# Activation de BuildKit pour les fonctionnalités avancées
export DOCKER_BUILDKIT=1

# Construction avec cache et sortie détaillée
docker build --progress=plain -t my-app:optimized .

# Analyse des couches de l'image
docker history my-app:optimized

# Inspection détaillée de l'image
docker inspect my-app:optimized
Sécurité des images

Scanner régulièrement les images pour détecter les vulnérabilités avec des outils comme Trivy ou Snyk. Les images de base doivent être mises à jour régulièrement pour inclure les correctifs de sécurité.

Networking avancé Docker

Docker offre plusieurs drivers réseau pour différents cas d'usage.

yaml
# docker-compose.networking.yml
version: "3.9"

services:
  # Frontend accessible publiquement
  frontend:
    build: ./frontend
    ports:
      - "80:80"
    networks:
      - frontend-network
      - backend-network

  # API accessible uniquement depuis le frontend
  api:
    build: ./api
    networks:
      - backend-network
      - database-network
    # Pas de ports exposés à l'extérieur

  # Base de données isolée
  database:
    image: postgres:16-alpine
    networks:
      - database-network
    # Accessible uniquement par l'API

networks:
  frontend-network:
    driver: bridge
  backend-network:
    driver: bridge
    internal: true  # Pas d'accès internet
  database-network:
    driver: bridge
    internal: true

Cette configuration isole les services selon le principe du moindre privilège. La base de données n'est accessible que par l'API.

bash
# terminal
# Inspection des réseaux Docker
docker network ls

# Détails d'un réseau spécifique
docker network inspect app-network

# Création d'un réseau personnalisé
docker network create --driver bridge --subnet 172.28.0.0/16 custom-network

# Connexion d'un conteneur à un réseau existant
docker network connect custom-network my-container

Volumes et persistance des données

Les volumes Docker préservent les données au-delà du cycle de vie des conteneurs.

yaml
# docker-compose.volumes.yml
version: "3.9"

services:
  app:
    image: my-app:latest
    volumes:
      # Volume nommé pour les données persistantes
      - app_data:/app/data
      # Montage bind pour le développement
      - ./uploads:/app/uploads:rw
      # Montage en lecture seule pour la configuration
      - ./config:/app/config:ro

  backup:
    image: alpine
    volumes:
      # Accès au même volume pour les sauvegardes
      - app_data:/data:ro
      - ./backups:/backups
    command: |
      sh -c "tar czf /backups/backup-$$(date +%Y%m%d).tar.gz /data"

volumes:
  app_data:
    driver: local
    driver_opts:
      type: none
      device: /path/to/host/data
      o: bind

La distinction entre volumes nommés et montages bind est importante : les volumes sont gérés par Docker tandis que les montages bind utilisent directement le système de fichiers hôte.

bash
# terminal
# Liste des volumes
docker volume ls

# Inspection d'un volume
docker volume inspect app_data

# Suppression des volumes orphelins
docker volume prune

# Sauvegarde d'un volume
docker run --rm -v app_data:/data -v $(pwd):/backup alpine \
  tar czf /backup/volume-backup.tar.gz /data

Déploiement en production

Un workflow de déploiement robuste inclut la construction, les tests et le push vers un registry.

bash
# deploy.sh
#!/bin/bash
set -e

# Variables
REGISTRY="registry.example.com"
IMAGE_NAME="my-app"
VERSION=$(git describe --tags --always)

echo "Building version: $VERSION"

# Construction de l'image de production
docker build \
  -f Dockerfile.production \
  -t $REGISTRY/$IMAGE_NAME:$VERSION \
  -t $REGISTRY/$IMAGE_NAME:latest \
  --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
  --build-arg VERSION=$VERSION \
  .

# Scan de sécurité
echo "Running security scan..."
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image $REGISTRY/$IMAGE_NAME:$VERSION

# Push vers le registry
echo "Pushing to registry..."
docker push $REGISTRY/$IMAGE_NAME:$VERSION
docker push $REGISTRY/$IMAGE_NAME:latest

echo "Deployment complete: $REGISTRY/$IMAGE_NAME:$VERSION"

Pour les déploiements sur un serveur, un fichier compose de production distinct permet d'adapter la configuration.

yaml
# docker-compose.prod.yml
version: "3.9"

services:
  app:
    image: registry.example.com/my-app:latest
    restart: always
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "0.5"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 256M
      update_config:
        parallelism: 1
        delay: 10s
        failure_action: rollback
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Cette configuration définit les ressources allouées, la stratégie de mise à jour et les healthchecks pour un déploiement fiable.

bash
# terminal
# Déploiement avec Docker Compose en production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Mise à jour sans interruption (rolling update)
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps app

# Rollback en cas de problème
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps \
  --scale app=0 && \
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps app

Monitoring et debugging des conteneurs

La surveillance des conteneurs est essentielle en production.

bash
# terminal
# Statistiques en temps réel de tous les conteneurs
docker stats

# Statistiques d'un conteneur spécifique avec format personnalisé
docker stats my-app --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

# Inspection des processus dans un conteneur
docker top my-app

# Événements Docker en temps réel
docker events --filter container=my-app

# Copie de fichiers depuis/vers un conteneur
docker cp my-app:/app/logs/error.log ./error.log

Pour un debugging approfondi, plusieurs techniques sont disponibles.

bash
# terminal
# Shell interactif dans un conteneur en cours d'exécution
docker exec -it my-app sh

# Exécution d'une commande unique
docker exec my-app cat /app/config/settings.json

# Démarrage d'un conteneur en mode debug
docker run -it --rm --entrypoint sh my-app:latest

# Inspection des variables d'environnement
docker exec my-app printenv

# Analyse des logs avec filtres
docker logs my-app --since 1h --tail 100 | grep ERROR
yaml
# docker-compose.monitoring.yml
version: "3.9"

services:
  app:
    # ... configuration existante
    labels:
      - "prometheus.scrape=true"
      - "prometheus.port=3000"
      - "prometheus.path=/metrics"

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=15d'

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=secret

volumes:
  prometheus_data:
  grafana_data:

Cette stack de monitoring permet de collecter et visualiser les métriques des conteneurs.

Conclusion

Docker transforme le cycle de développement en garantissant la cohérence entre tous les environnements. La conteneurisation apporte portabilité, isolation et reproductibilité, des qualités essentielles pour les applications modernes.

Checklist pour Docker en production

  • ✅ Multi-stage builds pour des images optimisées
  • ✅ Utilisateur non-root dans les conteneurs
  • ✅ Healthchecks configurés pour tous les services
  • ✅ Secrets gérés via Docker secrets ou variables d'environnement sécurisées
  • ✅ Limites de ressources (CPU, mémoire) définies
  • ✅ Volumes pour la persistance des données critiques
  • ✅ Logging centralisé avec rotation des fichiers
  • ✅ Scan de sécurité des images avant déploiement
  • ✅ Stratégie de mise à jour sans interruption
  • ✅ Réseau isolé entre les services

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

La maîtrise de Docker constitue une compétence fondamentale pour tout développeur moderne. De l'environnement local au déploiement en production, Docker standardise les workflows et simplifie les opérations. Les concepts présentés ici forment une base solide pour explorer ensuite Kubernetes et l'orchestration de conteneurs à grande échelle.

Tags

#docker
#containerization
#devops
#docker compose
#deployment

Partager

Articles similaires