Docker Compose : orchestrer des services multi-conteneurs en production

Guide pratique pour déployer Docker Compose en production avec PrestaShop : modélisation de la stack, images immuables, healthchecks, durcissement, sauvegardes et stratégies de déploiement.

Écran d'ordinateur avec code et icônes conceptuelles autour, lié à Docker Compose.

Table des matières :

  1. Docker Compose en production : ce que ça orchestre vraiment (et ce qu’il ne fera jamais)
  2. Modéliser une stack multi-conteneurs : services, réseaux, volumes, profils (exemple PrestaShop)
  3. Images immuables et chaîne de build : “build une fois”, déployer par tags/digests
  4. Démarrage fiable : healthchecks, dépendances, migrations, et mises à jour sans surprise
  5. Durcissement runtime : secrets, capacités Linux, FS en lecture seule, surface réseau
  6. Exploitation : logs, métriques, limites de ressources, backups, et performance PrestaShop

Docker Compose en production : ce que ça orchestre vraiment (et ce qu’il ne fera jamais)

Docker Compose reste un orchestrateur de nœud unique : il décrit une stack (services, réseaux, volumes) et pilote des conteneurs sur un seul hôte Docker. C’est suffisant pour beaucoup de productions e-commerce “mono-VPS” (reverse-proxy + PHP-FPM + DB + cache), mais il faut être lucide : Compose ne fournit ni scheduling multi-nœuds, ni autoscaling, ni rolling update natif, ni réconciliation de l’état à l’échelle d’un cluster. Docker résume bien l’objet de l’outil : « Compose is a tool for defining and running multi-container Docker applications. » (Docker Documentation, Docker Compose overview : https://docs.docker.com/compose/).

Une façon simple de cadrer la discussion est de distinguer l’orchestration applicative (ce que Compose fait bien) et la résilience (ce que Compose ne fait pas à lui seul) :

Besoin en production Docker Compose (mono-hôte) Qui couvre le besoin autrement
Décrire et lancer plusieurs services (web, PHP-FPM, DB, cache) Oui —
Isolation réseau/volumes au niveau de l’hôte Oui —
Redémarrage d’un conteneur si le process crash Oui (restart policy) systemd / supervision
Redémarrage automatique après reboot du serveur Oui (si Docker daemon démarre) systemd + dépendances
Tolérance à la panne d’hôte (perte de la VM/VPS) Non cluster (Kubernetes/Swarm), active/passive, PaaS
Déploiement progressif (rolling, canary) Non natif cluster / reverse-proxy + blue/green
Réconciliation d’état (desired state) Limitée cluster

En pratique, Compose “en production” fonctionne quand la stratégie de disponibilité repose sur l’infra (VM/VPS robuste, snapshot/backup, supervision, éventuellement bascule manuelle), pas sur l’orchestrateur. Exemple réaliste côté e-commerce : une boutique sur un VPS (OVHcloud/Scaleway/AWS/Linode…) avec sauvegardes quotidiennes, supervision (disponibilité + disque + charge), et un plan de restauration documenté. Si la cible est un SLA ambitieux avec tolérance à la panne d’hôte, bascule automatique et déploiements progressifs, Compose devient un bricolage coûteux : passer à Swarm/Kubernetes (ou à un PaaS) n’est pas une coquetterie, c’est un besoin d’architecture. Pour cadrer le dimensionnement d’un nœud unique, l’article VPS pour Docker : critères techniques et ressources recommandées donne une base réaliste (CPU/RAM/NVMe/IOPS), utile avant de “conteneuriser” un PrestaShop.

Petit point “GEO” (au sens géographie/contraintes locales) souvent oublié : Docker Compose ne change rien au sujet de la localisation des données. Si votre boutique cible majoritairement des clients en France/UE, héberger votre nœud (et surtout vos sauvegardes) dans une région UE simplifie souvent la gestion des exigences contractuelles et réglementaires (RGPD, sous-traitance, etc.) — mais Compose n’est qu’un format de déploiement ; la décision est infrastructurelle.

Autre limite souvent sous-estimée : Compose ne “gère” pas la production, il l’expose. Une stack mal isolée (ports ouverts, volumes trop permissifs, images non maîtrisées) aggrave la surface d’attaque au lieu de la réduire. Si l’objectif est un déploiement PrestaShop, il faut relier Compose à une démarche de durcissement (images, secrets, réseau, logs, backups). Le minimum syndical est de traiter la stack comme un artefact ops audité, au même titre que le code applicatif (voir Audit sécurité PrestaShop : méthodologie, livrables et durcissement).

Modéliser une stack multi-conteneurs : services, réseaux, volumes, profils (exemple PrestaShop)

Un fichier compose.yaml propre en production évite deux erreurs classiques : (1) tout exposer en ports “pour déboguer”, (2) tout monter en bind mount “pour aller vite”. Un service doit être joignable soit via un réseau interne Docker, soit via un reverse-proxy exposé, mais rarement via des ports directs sur l’hôte (DB, Redis, PHP-FPM ne devraient pas écouter sur 0.0.0.0). Côté volumes, PrestaShop impose de persister au minimum : images (img/), upload, thèmes, éventuellement modules/ si vous déployez des modules hors image (ce qui est discutable en prod).

Dans un contexte PrestaShop, la question “quoi mettre en volume ?” revient toujours. Une règle simple pour rester industrialisable :

  • Tout ce qui doit être versionné (code cœur, thèmes, modules, overrides) devrait idéalement être dans l’image et livré par CI/CD.
  • Tout ce qui est généré par l’activité (médias produits, fichiers uploadés, cache si vous ne le reconstruisez pas) doit être persisté.
  • Si vous montez un répertoire en RW “par confort” (ex. tout /var/www/html), vous réintroduisez de la dérive (snowflake server) : un patch manuel sur le serveur ne repassera pas en préprod, et la prochaine release peut écraser ou casser silencieusement.

Sur un PrestaShop 8.x/9.x, la séparation “propre” reste souvent : Nginx (statique + proxy vers PHP-FPM), PHP-FPM (code applicatif), MariaDB/MySQL (données), Redis (cache/sessions), plus un reverse-proxy TLS en frontal (Caddy/Traefik/HAProxy/Nginx) si vous mutualisez plusieurs sites. Pour Redis, il y a un vrai enjeu de sécurisation et de persistance : voir Redis sur Linux : installation, configuration et sécurisation production.

Exemple minimal (pattern “ports minimaux + réseaux internes + profils”) ; adapter les versions PHP/DB à la matrice PrestaShop (cf. PrestaShop 9 : versions PHP recommandées et incohérences de documentation) :

# compose.yaml (production mono-hôte)
services:
  nginx:
    image: nginx:stable-alpine
    depends_on:
      php:
        condition: service_healthy
    networks: [front, back]
    ports:
      - "127.0.0.1:8080:80" # exposé seulement en loopback, le TLS est au reverse-proxy
    volumes:
      - prestashop_public:/var/www/html:ro
      - prestashop_media:/var/www/html/img
      - ./ops/nginx/conf.d:/etc/nginx/conf.d:ro
    read_only: true
    tmpfs:
      - /var/cache/nginx
      - /var/run

  php:
    image: ghcr.io/votre-org/prestashop-php-fpm:9.0.0 # idéalement tag immuable + digest
    networks: [back]
    environment:
      APP_ENV: prod
    secrets:
      - db_password
    healthcheck:
      test: ["CMD", "php", "-v"]
      interval: 10s
      timeout: 3s
      retries: 6
    restart: unless-stopped

  db:
    image: mariadb:11
    command: ["--transaction-isolation=READ-COMMITTED", "--max-connections=300"]
    networks: [back]
    environment:
      MARIADB_DATABASE: prestashop
      MARIADB_USER: prestashop
      MARIADB_PASSWORD_FILE: /run/secrets/db_password
      MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
    secrets:
      - db_password
      - db_root_password
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
      interval: 5s
      timeout: 2s
      retries: 12

  redis:
    image: redis:7-alpine
    networks: [back]
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data

  adminer:
    image: adminer:latest
    profiles: ["ops"]
    networks: [back]
    ports:
      - "127.0.0.1:9090:8080"

networks:
  front:
  back:
    internal: true

volumes:
  prestashop_public:
  prestashop_media:
  db_data:
  redis_data:

secrets:
  db_password:
    file: ./secrets/db_password.txt
  db_root_password:
    file: ./secrets/db_root_password.txt

Points importants dans cet exemple : le réseau back est internal: true (pas de routage hors hôte), les ports sont en loopback quand il faut un accès ponctuel, et adminer est derrière un profile pour éviter de le lancer par défaut. En prod, ce genre d’outil d’admin devient vite une porte d’entrée inutile.

Deux garde-fous pratiques (souvent rentables dès le premier incident) :

  • Valider la stack avant déploiement : docker compose config (résolution des variables, cohérence YAML) et un “dry run” sur une VM de staging.
  • Documenter la topologie réseau : qui parle à qui, sur quel réseau Docker. Exemple fréquent d’erreur : un ports: "3306:3306" “temporaire” laissé en place, scanné puis attaqué (brute force, vulnérabilités, fuite d’info).

Images immuables et chaîne de build : “build une fois”, déployer par tags/digests

Faire un docker compose build directement sur le serveur est une anti-pattern en production : vous mélangez build et run, vous dépendez de la connectivité externe au moment du déploiement, et vous perdez la traçabilité (quelle base image, quels artifacts front, quels modules). La stratégie propre : BuildKit en CI, tests, scan, puis push dans un registry, et côté prod uniquement pull + up. Ça vaut particulièrement pour PrestaShop où le front (assets) et certains modules ajoutent une étape de build (Node/Yarn), rarement souhaitable sur une machine de prod.

L’objectif est d’obtenir des images immutables, reproductibles, et auditables. Concrètement : pinner les dépendances, éviter latest, idéalement pinner le digest (image: repo:tag@sha256:...), et produire une SBOM si votre chaîne le permet. Sur la partie sécurité images, l’article DISA : sécuriser les images conteneurisées selon le guide officiel est directement applicable : base minimaliste, packages strictement nécessaires, suppression des outils de debug, et processus de patching documenté.

En production e-commerce, la valeur d’une image “bien buildée” se voit surtout lors des changements :

  • Rollback rapide : si la release N casse le checkout, revenir à l’image N-1 doit être un changement de tag/digest, pas un “git pull” + bricolage.
  • Reproductibilité : vous pouvez reconstruire la même image (ou au moins expliquer ce qu’elle contient) des mois plus tard en cas d’audit.
  • Séparation des responsabilités : le serveur de prod n’a pas besoin d’outils de build (compilateurs, Node, git), ce qui réduit la surface d’attaque et le risque de dérive.

Côté configuration, le réflexe “12-factor” est utile mais doit être compris : « Store config in the environment » (The Twelve-Factor App, section Config : https://12factor.net/config). Oui pour les paramètres non sensibles (mode, URLs, flags), non pour les secrets : les variables d’environnement finissent dans des dumps, des logs, des UIs, ou des stacks traces. Compose sait monter des secrets sous /run/secrets ; si votre maturité le justifie, externalisez via un gestionnaire dédié (Vault, SOPS, etc.) et alimentez des fichiers secrets sur le host par un pipeline sécurisé.

Démarrage fiable : healthchecks, dépendances, migrations, et mises à jour sans surprise

Le piège depends_on : il garantit l’ordre de création, pas la disponibilité applicative. Sans healthcheck, votre conteneur PHP peut démarrer alors que MariaDB n’a pas fini le recovery InnoDB, et PrestaShop va générer des erreurs “connexion DB” au démarrage (et parfois des effets secondaires si un install/upgrade s’exécute). La bonne pratique est d’utiliser healthcheck + depends_on: condition: service_healthy, et de piloter les déploiements avec docker compose up -d --wait (option disponible dans Compose v2) pour ne pas “déclarer” une mise en prod tant que les services critiques ne sont pas sains.

La définition de liveness est un sujet : un php -v ou un curl / ne valide pas que PrestaShop est prêt (cache warmup, migrations, indexation, connexions Redis, etc.). Docker rappelle l’intention : « The HEALTHCHECK instruction tells Docker how to test a container to check that it is still working. » (Dockerfile reference, HEALTHCHECK : https://docs.docker.com/reference/dockerfile/#healthcheck). Pour PrestaShop, un endpoint dédié (ex: /healthz servi par Nginx, test DB/Redis simple, sans authent) est plus fiable qu’un simple ping HTTP sur /.

Deux patterns utiles sur mono-hôte pour éviter les démarrages “semi-fonctionnels” :

  • Une étape “job” explicite pour les opérations non idempotentes (migrations, warmup). Même sans ajouter de nouveaux outils, vous pouvez lancer un conteneur éphémère : docker compose run --rm php ... avant de basculer le trafic.
  • Un timeout maîtrisé : si DB n’est pas saine en X secondes, vous préférez échouer clairement (et alerter) plutôt que de servir un site instable. Côté Nginx/HAProxy, cela passe aussi par des checks et des timeouts cohérents.

Pour les mises à jour, Compose ne fait pas de rolling update. La mécanique standard est :

docker compose pull
docker compose up -d --remove-orphans

Ça redémarre les conteneurs, donc coupure (courte) si vous n’avez pas de stratégie de bascule. Une option pragmatique sur mono-hôte : blue/green par projet Compose, avec deux stacks (--project-name shop_blue / shop_green) et un reverse-proxy capable de basculer de backend (HAProxy/Nginx). Pour HAProxy, voir HAProxy 3.2 : déploiement LTS sur Ubuntu/Debian, compilation et systemd ; et pour les patterns de bascule côté applicatif, la logique est proche de Migration PrestaShop : audit technique et plan incrémental blue/green.

Dans un scénario boutique “à pics” (soldes, TV, influence), le point dur n’est pas seulement le redémarrage : ce sont les effets de cache (cache froid après restart) et la DB (migrations/index) qui peuvent générer une dégradation visible. D’où l’intérêt de planifier les déploiements (fenêtre creuse, préchauffage cache, surveillance active sur 5xx et latences DB) plutôt que de considérer up -d comme une “mise à jour sans risque”.

Durcissement runtime : secrets, capacités Linux, FS en lecture seule, surface réseau

En production, le durcissement commence par ce que Compose rend trop facile : exposer, monter, et exécuter en root. Réduire la surface : n’exposer que 80/443 au reverse-proxy, mettre DB/Redis sur réseau interne, et éviter les bind mounts “full project” en RW. Si le code PrestaShop est packagé dans l’image (recommandé), montez uniquement les répertoires qui doivent persister (médias, éventuellement overrides/thèmes si votre pipeline l’impose). Côté MySQL/MariaDB, ne mappez pas le port 3306 et utilisez des comptes applicatifs à privilèges minimaux (et derrière, surveillez et optimisez : PrestaShop MySQL : analyser slow query log et optimiser index).

Pour les secrets : utilisez *_FILE quand l’image le supporte (MariaDB, beaucoup d’images officielles), sinon lisez le secret depuis /run/secrets/... dans votre entrypoint. Évitez de laisser traîner des fichiers secrets world-readable sur le host : permissions 0600, ownership dédié, et rotation documentée. Si vous gérez aussi des clusters Kubernetes, la logique “source de vérité centralisée” (KMS + synchro) est décrite dans External Secrets Operator : synchroniser les secrets Kubernetes avec OVHcloud OKMS ; sur Compose, l’équivalent est moins standardisé, mais le principe (ne pas versionner les secrets, rotation, audit) reste identique.

Pour le runtime Linux : appliquez le minimum viable (en testant, parce que PrestaShop/FFMPEG/ImageMagick/cron peuvent casser). Typiquement : read_only: true + tmpfs pour les répertoires temporaires, cap_drop: ["ALL"] (puis réajouter si besoin), security_opt: ["no-new-privileges:true"], pids_limit, ulimits, et un user non-root dans l’image (USER www-data ou UID dédié).

Voici un exemple “patch” (à adapter) qui illustre l’esprit, sans promettre que tout passera du premier coup sur votre stack (notamment si vous écrivez des fichiers temporaires au mauvais endroit) :

services:
  php:
    user: "33:33" # www-data sur Debian/Alpine selon images, à vérifier
    read_only: true
    tmpfs:
      - /tmp
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    pids_limit: 256
    stop_grace_period: 30s
    init: true

Complétez par un WAF/filtrage sur le reverse-proxy si le contexte l’exige (checkout, bots, faux positifs) : WAF PrestaShop : réduire les faux positifs et sécuriser le checkout. Et côté applicatif, n’oubliez pas que la conteneurisation ne corrige pas les failles XSS/IDOR : XSS : checklist de durcissement PrestaShop, CSP et encodage contexte et Broken access control : prévenir IDOR et élévation de privilèges.

Pour une checklist “sécurité conteneurs” plus large (complémentaire à vos guides internes), la Docker Security Cheat Sheet d’OWASP est une bonne référence : https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html.

Exploitation : logs, métriques, limites de ressources, backups, et performance PrestaShop

Sans discipline logs/metrics, Compose en production devient une boîte noire. Par défaut, Docker écrit des logs JSON locaux : configurez une rotation (logging.options.max-size/max-file) pour éviter de remplir le disque (classique sur VPS), ou branchez un driver centralisé (journald/syslog) selon vos pratiques. Une configuration de rotation “simple et efficace” ressemble souvent à :

services:
  nginx:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

Ajoutez une métrique minimale : CPU, RSS, IO, latences DB, et erreurs 5xx au proxy. La partie “performance applicative” ne disparaît pas en conteneurs : sur PrestaShop, l’OPcache et le cache Smarty restent déterminants (voir PHP OPcache : paramètres recommandés pour optimiser les performances et PrestaShop : optimiser performance via cache Smarty, CCC et CDN).

Les limites de ressources doivent être explicites, sinon c’est le kernel qui arbitrera (OOM killer, throttling imprévisible). Avec Compose, fixez au moins des bornes mémoire et une politique de redémarrage. Attention à un point pratique : selon votre version de Docker/Compose, certaines clés “mode Swarm” (section deploy) ne sont pas appliquées par défaut en mode Compose classique. Si vous vous appuyez sur des limites, testez-les réellement (docker stats, observation OOM) et privilégiez les paramètres pris en compte par votre environnement.

Pour les workloads e-commerce, la DB et PHP-FPM sont les premiers candidats à cadrer : mem_limit/cpus (selon le mode), ulimits.nofile, innodb_buffer_pool_size côté DB, et pool PHP-FPM dimensionné. Quand ça sature, les symptômes sont identiques : pages lentes, timeouts, et Core Web Vitals dégradés (corrélez infra + SEO technique si vous pilotez le trafic : PrestaShop performance : optimiser gros catalogues et Core Web Vitals).

Enfin, la production = restauration testée, pas “backup configuré”. Avec Compose, il faut sauvegarder les volumes (DB + médias) et pas juste le répertoire du projet. Une approche opérationnelle (et testable) consiste à définir clairement :

  • RPO (Recovery Point Objective) : combien de minutes/heures de commandes vous acceptez de perdre.
  • RTO (Recovery Time Objective) : combien de temps vous acceptez d’être indisponible.

Pour MariaDB, combinez dump logique (mysqldump --single-transaction) et, si possible, snapshot bloc (LVM/ZFS) pour réduire la fenêtre ; l’article API OVHcloud : configurer LVM et ZFS lors d’une réinstallation serveur aide si vous contrôlez la couche stockage. Testez une restauration sur une stack isolée (--project-name restore_test) et validez : démarrage complet, accès BO/FO, intégrité des images produits, et absence d’erreurs DB.

Checklist de test de restauration (simple, mais très efficace) :

  • Le dump DB se restaure sans erreur et la boutique démarre sans lancer d’install/upgrade inattendu.
  • Les médias produits sont présents (vérifier une catégorie “ancienne”, pas seulement les derniers imports).
  • La connexion Redis (si activée pour cache/sessions) fonctionne et n’induit pas de boucles de cache.
  • Les emails transactionnels ne partent pas en boucle sur l’environnement de test (désactiver/enregistrer).
  • Le reverse-proxy sert bien le bon backend (utile en blue/green).

C’est la seule façon de savoir si votre “orchestration multi-conteneurs en production” survivra à un incident réel.


À lire aussi