Aller au contenu

Stack Docker sur VPS — staging + prod derrière un proxy partagé

Objectif : structurer le côté serveur du déploiement : proxy Nginx partagé, stacks staging et prod isolées, script de déploiement piloté par le CI. Quand l'utiliser : en complément du runbook pipeline-gitlab-deploiement.md, à la préparation d'un VPS qui héberge staging + prod du même projet. Templates associés : deploy.sh, docker-compose.stack.yml, docker-compose.proxy.yml.

Vue d'ensemble

Un seul VPS héberge staging et prod côte à côte, isolés, derrière un proxy Nginx commun :

Internet :80/:443
        │
   [ <PROJET>_proxy ]  (nginx, démarré 1 fois, réseau <PROJET>_proxy_net)
     │            │
 staging.<dom>   <dom> / app.<dom>
     │            │
 [staging_web]  [prod_web]        ← uniquement sur proxy_net (R3)
     │            │
 [staging_api]  [prod_api]        ← sur proxy_net + réseau privé
     │            │
 [staging_net]  [prod_net]  ← postgres / redis / minio (jamais exposés)
  • Réseaux séparés par environnement (<PROJET>_staging_net, <PROJET>_prod_net) : staging et prod ne se voient pas.
  • proxy_net externe : partagé, c'est le seul point de contact avec le proxy.
  • Le CI ne recrée que api + web (rolling update). La data (postgres/redis/minio) n'est jamais touchée par un déploiement.

Arborescence sur le VPS

/opt/<PROJET>/
├── nginx/
│   ├── nginx.conf
│   └── conf.d/                 # vhosts staging + prod
├── staging/
│   ├── docker-compose.yml      # (copie de docker-compose.stack.yml, variante staging)
│   ├── .env                    # secrets staging (jamais versionné)
│   └── deploy-staging.sh
└── prod/
    ├── docker-compose.yml      # variante prod
    ├── .env                    # secrets prod (jamais versionné)
    └── deploy-prod.sh

Préparation du VPS (une seule fois)

  1. Durcir le VPS (voir ../vps/durcissement-vps-ovh.md) : user deploy, port SSH custom, Docker installé.
  2. Créer l'arborescence /opt/<PROJET>/{nginx/conf.d,staging,prod}.
  3. Déposer les fichiers .env dans staging/ et prod/ (secrets DB, Redis, MinIO, PORT, etc.) — permissions 600, valeurs staging ≠ prod.
  4. Configurer les vhosts Nginx dans nginx/conf.d/ (staging.conf, prod.conf) + certificats Let's Encrypt (certbot).
  5. Démarrer le proxy partagé (crée le réseau <PROJET>_proxy_net) :
    docker compose -f docker-compose.proxy.yml up -d
    

Ensuite, chaque git push déclenche le pipeline qui pousse les images et exécute deploy.sh côté serveur (voir runbook pipeline).

Règles de durcissement Docker (à conserver)

Ces règles sont intégrées dans docker-compose.stack.yml :

  1. USER non-root dans le Dockerfile applicatif (api/web).
  2. Versions d'images tierces épinglées (postgres, redis, minio) — jamais latest non maîtrisé.
  3. web hors du réseau privé : il ne peut pas joindre la DB/Redis directement, il passe par l'API.
  4. Limites CPU/mémoire sur chaque service (deploy.resources.limits).
  5. Rotation des logs (json-file, max-size + max-file) — évite la saturation disque.
  6. no-new-privileges:true partout ; cap_drop: ALL sauf exceptions justifiées.

⚠️ Exception connue : Redis ne supporte pas cap_drop: ALL — il a besoin de CAP_SETUID/CAP_SETGID pour basculer vers l'utilisateur redis. Documenté dans le template.

Le script de déploiement (deploy.sh)

Piloté par la variable ENV (staging/prod), il : 1. se logue au registry, 2. (staging) purge les conteneurs orphelins, 3. pull api:<tag> et web:<env>-<tag>, 4. recrée uniquement api/web (--no-deps --force-recreate --pull never), 5. recharge Nginx (nginx -t && nginx -s reload), 6. nettoie les vieilles images.

Un trap ... ERR dump les logs de l'API et l'état des conteneurs si le déploiement échoue → diagnostic direct dans la sortie du job CI.

Vérification

  • [ ] docker compose -f docker-compose.proxy.yml ps → proxy Up
  • [ ] Après un deploy : docker ps --filter name=staging → api/web healthy
  • [ ] staging.<domaine> et <domaine> répondent en HTTPS
  • [ ] postgres/redis/minio non accessibles depuis l'extérieur (aucun port publié)
  • [ ] Les .env sont en 600 et hors Git

Dépannage

  • network <PROJET>_proxy_net declared as external ... not found → le proxy n'a pas été démarré (étape 5).
  • api unhealthy → healthcheck : vérifier PORT dans .env et l'endpoint /api/health. Le trap du deploy affiche les logs.
  • 502 Bad Gateway → le conteneur cible n'est pas sur proxy_net, ou nom/upstream Nginx erroné.
  • Redis ne démarre pas → cap_drop: ALL ajouté par erreur (voir exception ci-dessus).

Ressources

  • Runbook pipeline CI/CD : pipeline-gitlab-deploiement.md
  • Runbook durcissement VPS : ../vps/durcissement-vps-ovh.md
  • Docker Compose (networks, deploy) : https://docs.docker.com/compose/

Dernière mise à jour : 2026-07-02