Ajouter un nouveau projet sur un VPS déjà dockerisé (Caddy + Watchtower)
Procédure minimale pour ajouter un service supplémentaire à une infra existante : dossier, env, Compose, labels Caddy et DNS.
Ce guide décrit la procédure minimale pour ajouter un nouveau projet à un VPS déjà configuré avec :
- un Caddy frontal (reverse proxy central) → voir Caddy en frontal : un reverse proxy unique pour nos apps ;
- Watchtower en mode auto-update → voir Automatiser les mises à jour de vos conteneurs Docker avec Watchtower et Watchtower avec GHCR privé : authentification et sélection par labels ;
- une chaîne CI → GHCR → Watchtower → Prod déjà opérationnelle → voir Chaîne complète : CI → GHCR privé → Watchtower → Prod.
🎯 Objectif : déployer un nouveau service applicatif en réutilisant l'infra existante (sans retoucher Caddy frontal ni Watchtower).
1) Organisation des dossiers
Isoler chaque application dans son propre répertoire (lisibilité, déploiements indépendants) :
~/infra/ # Caddy frontal + Watchtower (déjà en place)
~/blog/ # projet existant
~/nouveau-projet/
└─ api/ # service à ajouter (ex: API)
2) Cloner le dépôt
cd ~
mkdir -p ~/nouveau-projet
cd ~/nouveau-projet
git clone git@github.com:<user>/<repo>.git api
cd api
3) Variables d'environnement
Créer un .env.prod local au serveur (non commité) :
cat > .env.prod <<'ENV'
APP_ENV=prod
APP_DEBUG=0
APP_SECRET=$(php -r 'echo bin2hex(random_bytes(32));')
PORT=8080
# DB_*, JWT_*, etc. si nécessaire
ENV
chmod 600 .env.prod
Pour générer manuellement APP_SECRET :
php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;'
# ou
openssl rand -hex 32
4) Docker Compose du service
Le Caddy frontal découvre le service via labels (caddy-docker-proxy).
Le service expose son HTTP interne (ex. Caddy interne/FrankenPHP sur 8080), sans publier de port hôte.
# compose.prod.yaml
services:
api:
image: ghcr.io/<user>/<repo>:main
restart: unless-stopped
env_file:
- .env.prod
networks: [edge] # réseau partagé déjà utilisé par le Caddy frontal
# expose: ["8080"] # utile si l'image ne publie pas déjà 8080
labels:
# Watchtower: suivi explicite (voir article labels)
com.centurylinklabs.watchtower.enable: "true"
# Caddy frontal : domaine public -> proxy vers le port interne
caddy: api.mon-projet.com
caddy.encode: gzip
caddy.reverse_proxy: "{{upstreams 8080}}"
# Sécurité HTTP minimale
caddy.header.Strict-Transport-Security: "max-age=31536000; includeSubDomains; preload"
caddy.header.X-Content-Type-Options: "nosniff"
caddy.header.X-Frame-Options: "DENY"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz || exit 1"]
interval: 20s
timeout: 3s
retries: 3
networks:
edge:
external: true
📎 Fondamentaux :
- Aucun
ports:côté service (le frontal s'en charge). - Réseau
edgeidentique à celui déclaré dans l'infra (Caddy frontal). - Label Watchtower si vous filtrez par
--label-enable(cf. article labels).
5) DNS
Créer (ou mettre à jour) un enregistrement A/CNAME pour le domaine public du service :
api.mon-projet.com → [IP du VPS]
Le Caddy frontal émettra automatiquement un certificat TLS (Let's Encrypt) au premier passage.
6) Premier déploiement
docker compose -f compose.prod.yaml up -d
Vérifications :
docker ps
docker logs -f infra-caddy-1 # Caddy frontal: découverte + certificat
curl -I https://api.mon-projet.com/healthz
Un HTTP/2 200 confirme la publication.
7) CI/CD (rappel rapide)
Si la chaîne CI → GHCR → Watchtower est déjà configurée, chaque push sur la branche principale régénère l'image :main et Watchtower redéploie le service automatiquement (aucune action manuelle).
Pour les détails et variants (tags, multi-arch, permissions), voir Chaîne complète : CI → GHCR privé → Watchtower → Prod.
8) Dépannage éclair
- 404/502 via le frontal → vérifier
caddy.reverse_proxyet le port interne (8080 vs 8000, etc.). - TLS/Let's Encrypt → attendre la propagation DNS ; contrôler les logs de
infra-caddy-1. - Watchtower n'actualise pas → confirmer le label
com.centurylinklabs.watchtower.enable: "true"et la présence des identifiants GHCR (voir Watchtower avec GHCR privé). - Symfony .env → inclure un
.envnon sensible dans l'image et surcharger via.env.prod(voir remarques dans Chaîne complète).