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_netexterne : 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)¶
- Durcir le VPS (voir
../vps/durcissement-vps-ovh.md) : userdeploy, port SSH custom, Docker installé. - Créer l'arborescence
/opt/<PROJET>/{nginx/conf.d,staging,prod}. - Déposer les fichiers
.envdansstaging/etprod/(secrets DB, Redis, MinIO,PORT, etc.) — permissions600, valeurs staging ≠ prod. - Configurer les vhosts Nginx dans
nginx/conf.d/(staging.conf, prod.conf) + certificats Let's Encrypt (certbot). - 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 :
- USER non-root dans le Dockerfile applicatif (api/web).
- Versions d'images tierces épinglées (postgres, redis, minio) — jamais
latestnon maîtrisé. webhors du réseau privé : il ne peut pas joindre la DB/Redis directement, il passe par l'API.- Limites CPU/mémoire sur chaque service (
deploy.resources.limits). - Rotation des logs (
json-file, max-size + max-file) — évite la saturation disque. no-new-privileges:truepartout ;cap_drop: ALLsauf exceptions justifiées.
⚠️ Exception connue : Redis ne supporte pas
cap_drop: ALL— il a besoin deCAP_SETUID/CAP_SETGIDpour basculer vers l'utilisateurredis. 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→ proxyUp - [ ] Après un deploy :
docker ps --filter name=staging→ api/webhealthy - [ ]
staging.<domaine>et<domaine>répondent en HTTPS - [ ] postgres/redis/minio non accessibles depuis l'extérieur (aucun port publié)
- [ ] Les
.envsont en600et 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érifierPORTdans.envet l'endpoint/api/health. Letrapdu 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: ALLajouté 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