Déploiement : CI/CD GitLab + image frontend de prod + Caddy

Remplace la chaîne GitHub Actions (jamais réellement branchée : aucun
remote git n'existait) par une chaîne GitLab CI/CD, et comble un manque
qu'elle avait déjà : le frontend n'était jusqu'ici jamais construit ni
déployé en production.

- `.gitlab-ci.yml` : tests (backend + frontend) à chaque push ; sur `main`,
  build des images backend et frontend (registre de conteneurs GitLab) puis
  déploiement SSH sur le VPS (pull, migration Alembic, up -d).
- `frontend/Dockerfile.prod` + `frontend/nginx.conf` : build Vite statique
  servi par nginx (repli SPA sur index.html). `VITE_API_MODE`/
  `VITE_EDITION_ID` sont des variables de build, passées en `--build-arg`
  depuis la variable CI/CD `VITE_EDITION_ID`.
- `deploy/docker-compose.prod.yml` : ajout du service `frontend` ; `backend`
  et `frontend` ne publient plus que sur `127.0.0.1` (Caddy, déjà installé
  sur le serveur, devient l'unique porte d'entrée publique).
- `deploy/Caddyfile.benevoles` : bloc de vhost à ajouter au Caddy existant
  (`/api/*` vers le backend avec préfixe retiré, le reste vers le frontend).
- `deploy/README.md` : runbook complet (remote GitLab, variables CI/CD,
  préparation du VPS, création de l'édition de production avant le premier
  build frontend, vhost Caddy).
- CLAUDE.md et README.md mis à jour en conséquence.

Vérifié : `docker compose -f deploy/docker-compose.prod.yml config`,
build réel de l'image frontend de prod, / et une route SPA (/grille)
répondent 200 via nginx.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
AdamThomas
2026-09-14 19:04:28 +02:00
co-authored by Claude Sonnet 5
parent 367dfe42eb
commit d5f67b2701
9 changed files with 333 additions and 135 deletions
+24
View File
@@ -0,0 +1,24 @@
# Bloc à ajouter au Caddyfile existant du serveur (ex. /etc/caddy/Caddyfile,
# ou un fichier importé par lui — voir deploy/README.md). Remplacer le nom de
# domaine par le vôtre. Caddy gère seul le certificat HTTPS (Let's Encrypt).
#
# Le frontend (nginx, port 8080) et le backend (port 8000) écoutent tous les
# deux sur 127.0.0.1 uniquement (voir docker-compose.prod.yml) : ce vhost est
# leur unique porte d'entrée publique.
benevoles.mon-domaine.fr {
encode zstd gzip
# API : même préfixe `/api` que le proxy Vite en développement, retiré
# avant de transmettre au backend (qui expose ses routes sous
# `/editions/...`, pas `/api`).
handle /api/* {
uri strip_prefix /api
reverse_proxy 127.0.0.1:8000
}
# Reste de l'application : le frontend statique (nginx).
handle {
reverse_proxy 127.0.0.1:8080
}
}
+108 -20
View File
@@ -1,24 +1,46 @@
# Déploiement
Le workflow [`.github/workflows/deploy.yml`](../.github/workflows/deploy.yml)
se déclenche sur **chaque push sur `main`** :
Le pipeline [`.gitlab-ci.yml`](../.gitlab-ci.yml) se déclenche sur **chaque
push** :
1. build de l'image `backend` ;
2. push sur `ghcr.io/<owner>/<repo>-backend` (tags `latest` et `sha-<commit>`) ;
1. tests (backend : `ruff` + `pytest` + `alembic check` ; frontend :
`typecheck` + `lint` + `build` + `smoke`) ;
2. sur `main` seulement : build des images `backend` et `frontend`, poussées
sur le registre de conteneurs du projet GitLab ;
3. copie de `deploy/docker-compose.prod.yml` sur le VPS par SSH ;
4. `docker compose -f docker-compose.prod.yml pull && up -d`.
4. `docker compose pull`, migration Alembic, `up -d`.
## Secrets GitHub Actions requis
Le VPS n'expose rien directement : un **Caddy déjà installé sur le
serveur** (service système, hors de ce dépôt) reçoit le trafic public sur
443 et reverse-proxy vers les deux conteneurs, qui n'écoutent que sur
`127.0.0.1`.
| Secret | Description |
|----------------|----------------------------------------------------------------|
| `SSH_HOST` | Hôte du VPS |
| `SSH_USER` | Utilisateur SSH |
| `SSH_KEY` | Clé privée SSH (format OpenSSH) |
| `DEPLOY_PATH` | Dossier du projet sur le VPS (ex. `/opt/benevoles`) |
| `GHCR_TOKEN` | PAT avec scope `read:packages`, pour que le VPS tire l'image |
## Étape 0 — pousser le dépôt sur GitLab
## Préparation du VPS (une fois)
```bash
# Créer d'abord un projet vide sur GitLab (sans README ni .gitignore), puis :
git remote add origin git@gitlab.com:<namespace>/<projet>.git
git push -u origin main
```
## Variables CI/CD à définir (Paramètres → CI/CD → Variables)
| Variable | Description |
|-------------------|-------------------------------------------------------------------|
| `SSH_HOST` | Hôte du VPS |
| `SSH_USER` | Utilisateur SSH |
| `SSH_KEY` | Clé privée SSH (format OpenSSH) — **masquée**, sur `main` seulement|
| `DEPLOY_PATH` | Dossier du projet sur le VPS (ex. `/opt/benevoles`) |
| `VITE_EDITION_ID` | UUID de l'édition de production — **à ajouter après** l'étape 3 |
`CI_REGISTRY`, `CI_REGISTRY_USER`, `CI_REGISTRY_PASSWORD` et
`CI_REGISTRY_IMAGE` sont fournies automatiquement par GitLab, rien à
configurer.
La clé publique correspondant à `SSH_KEY` doit être dans le
`~/.ssh/authorized_keys` de `SSH_USER` sur le VPS.
## Étape 1 — préparation du VPS (une fois)
```bash
mkdir -p /opt/benevoles
@@ -28,21 +50,87 @@ POSTGRES_USER=benevoles
POSTGRES_PASSWORD=<mot de passe fort>
POSTGRES_DB=benevoles
JWT_SECRET=<secret fort ; openssl rand -hex 32>
PUBLIC_BASE_URL=https://benevoles.mon-festival.fr
PUBLIC_BASE_URL=https://benevoles.mon-domaine.fr
# Emails (récapitulatifs de planning + liens de connexion). Laisser
# EMAIL_ENABLED=false tant que le SMTP n'est pas prêt : les emails sont alors
# seulement journalisés.
EMAIL_ENABLED=true
SMTP_HOST=smtp.mon-festival.fr
SMTP_HOST=smtp.mon-domaine.fr
SMTP_PORT=587
SMTP_USER=no-reply@mon-festival.fr
SMTP_USER=no-reply@mon-domaine.fr
SMTP_PASSWORD=<mot de passe SMTP>
SMTP_STARTTLS=true
SMTP_SSL=false
SMTP_FROM=Festival <no-reply@mon-festival.fr>
SMTP_FROM=Festival <no-reply@mon-domaine.fr>
EOF
```
Le workflow dépose `docker-compose.prod.yml` dans ce dossier et l'utilise tel
quel.
Le pipeline dépose `docker-compose.prod.yml` dans ce dossier et l'utilise tel
quel — il n'y a rien d'autre à y copier.
## Étape 2 — premier déploiement (backend + base seuls)
Le frontend a besoin de l'UUID d'une édition pour être construit (voir plus
bas) : le tout premier déploiement se fait donc en deux temps.
1. Sur GitLab, lancer le pipeline sur `main` normalement (Push → CI/CD →
Pipelines, ou un simple commit). Le job `build-frontend` échouera ou
produira une image sans édition tant que `VITE_EDITION_ID` n'existe pas :
c'est attendu, on l'ignore pour l'instant.
2. Une fois `build-backend` et `deploy` passés, `db` et `backend` tournent
sur le VPS.
## Étape 3 — créer l'édition de production
```bash
ssh SSH_USER@SSH_HOST
cd /opt/benevoles
docker compose -f docker-compose.prod.yml run --rm backend \
uv run python -m app.bootstrap \
--name "Festival 2026" --start 2026-07-01 --end 2026-07-04 \
--email coordination@mon-domaine.fr --password <mot-de-passe-solide>
```
La commande affiche l'UUID de l'édition créée. Récupérer aussi celui-ci à
tout moment avec :
```bash
docker compose -f docker-compose.prod.yml exec -T db \
psql -U benevoles -tAc "select id from edition order by created_at desc limit 1"
```
## Étape 4 — brancher le frontend sur cette édition
Dans GitLab, ajouter la variable CI/CD `VITE_EDITION_ID` avec cet UUID, puis
relancer le pipeline sur `main` (bouton *Retry* sur le dernier pipeline, ou un
nouveau commit). Cette fois `build-frontend` construit une image qui pointe
sur la bonne édition, et `deploy` démarre le service `frontend`.
`VITE_EDITION_ID` ne change plus ensuite : elle ne se touche à nouveau que
pour une nouvelle édition (festival de l'année suivante) qu'on voudrait
publier sur ce même domaine.
## Étape 5 — le vhost Caddy
Caddy tourne déjà sur ce serveur (service système) : ajouter le bloc
[`deploy/Caddyfile.benevoles`](./Caddyfile.benevoles) à son `Caddyfile` (ou au
fichier qu'il importe pour les sites), avec le vrai nom de domaine, puis :
```bash
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
Le DNS du domaine doit déjà pointer (enregistrement `A`/`AAAA`) vers ce VPS —
Caddy obtient et renouvelle seul le certificat HTTPS (Let's Encrypt) au
premier accès.
Vérifier ensuite que `PUBLIC_BASE_URL` dans le `.env` du VPS (étape 1)
correspond bien à `https://<ce domaine>` — c'est cette valeur qui est utilisée
dans les liens des emails envoyés aux bénévoles.
## Déploiements suivants
Un simple push sur `main` suffit : tests → build → déploiement → migration →
redémarrage, sans aucune intervention sur le VPS ni sur Caddy.
+16 -3
View File
@@ -3,7 +3,12 @@ name: benevoles
# Déployé sur le VPS. Les variables sont lues depuis un fichier .env situé à
# côté de ce fichier (non versionné) : POSTGRES_USER, POSTGRES_PASSWORD,
# POSTGRES_DB, JWT_SECRET, PUBLIC_BASE_URL et les variables SMTP_* / EMAIL_ENABLED.
# BACKEND_IMAGE est exporté par le workflow de déploiement.
# BACKEND_IMAGE / FRONTEND_IMAGE sont exportées par le pipeline de déploiement
# (.gitlab-ci.yml).
#
# Les deux services ne publient que sur `127.0.0.1` : c'est le Caddy du VPS
# (hors de ce compose, voir deploy/README.md) qui reçoit le trafic public et
# reverse-proxy vers ces ports.
services:
db:
@@ -22,7 +27,7 @@ services:
retries: 10
backend:
image: ${BACKEND_IMAGE:-ghcr.io/OWNER/REPO-backend:latest}
image: ${BACKEND_IMAGE:-registry.gitlab.com/OWNER/REPO/backend:latest}
environment:
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
ENVIRONMENT: production
@@ -41,7 +46,15 @@ services:
db:
condition: service_healthy
ports:
- "8000:8000"
- "127.0.0.1:8000:8000"
restart: unless-stopped
frontend:
image: ${FRONTEND_IMAGE:-registry.gitlab.com/OWNER/REPO/frontend:latest}
depends_on:
- backend
ports:
- "127.0.0.1:8080:80"
restart: unless-stopped
volumes: