Files
benevoles/deploy
AdamThomas 010cac8adc
ci-cd / backend-test (push) Failing after 7m57s
ci-cd / build-backend (push) Skipped
ci-cd / frontend-test (push) Failing after 32s
ci-cd / build-frontend (push) Skipped
ci-cd / deploy (push) Skipped
gitea ci
2026-09-16 21:09:28 +02:00
..
2026-09-16 21:09:28 +02:00
2026-09-16 21:09:28 +02:00

Déploiement

Le pipeline .gitea/workflows/ci-cd.yml (Gitea Actions) se déclenche sur chaque push :

  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 intégré à Gitea ;
  3. copie de deploy/docker-compose.prod.yml sur le VPS par SSH ;
  4. docker compose pull, migration Alembic, up -d.

Nécessite un runner Gitea Actions (act_runner, exécuteur Docker) activé sur l'instance et attaché au dépôt.

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.

Avant d'ouvrir au public — check-list sécurité

  • JWT_SECRET : une vraie valeur générée (openssl rand -hex 32), jamais la valeur par défaut de .env.example. Le backend refuse de démarrer en ENVIRONMENT=production sinon (app/config.py:check_production_secrets).
  • EMAIL_ENABLED=true avec un SMTP qui fonctionne (docker compose run --rm backend uv run python -m app.email.check <destinataire> pour vérifier) — sinon les liens magiques sont journalisés en clair au lieu d'être envoyés (le backend avertit au démarrage tant que c'est le cas, voir plus bas).
  • Édition créée via python -m app.bootstrap (étape 3), jamais via python -m app.seed : ce dernier crée un compte de démonstration (coordination@lucioles.test) avec un mot de passe versionné dans le dépôt — un vrai identifiant de connexion COORDINATION public si la base de démo devient la base de production.
  • CORS_ORIGINS : sans objet ici tant que le frontend appelle l'API en même origine via /api/* (voir le vhost Caddy, étape 5) — à ne renseigner que si un autre client (domaine séparé, appli mobile) doit appeler l'API directement.
  • /auth/login et /auth/magic-link sont limités en tentatives par email (5/min et 3/5 min) — voir app/auth/rate_limit.py. Limité par email et non par IP (le VPS ne transmet pas encore l'IP réelle du client à travers Caddy) : suffisant contre la force brute sur un compte connu, pas contre une attaque distribuée sur de nombreux emails différents.

Étape 0 — pousser le dépôt sur Gitea

# Créer d'abord un dépôt vide sur Gitea (sans README ni .gitignore), puis :
git remote add origin git@gitea.mon-domaine.fr:<namespace>/<projet>.git
git push -u origin main

Secrets et variables à définir (Paramètres du dépôt → Actions)

Secrets (Paramètres → Actions → Secrets) :

Secret Description
REGISTRY_TOKEN Jeton d'accès (scope write:package) pour pousser sur le registre de conteneurs de l'instance Gitea
SSH_KEY Clé privée SSH (format OpenSSH)

Variables (Paramètres → Actions → Variables) :

Variable Description
REGISTRY_HOST Hôte du registre (ex. gitea.mon-domaine.fr)
REGISTRY_NAMESPACE Espace du registre (ex. <organisation>/<depot>)
REGISTRY_USER Utilisateur associé à REGISTRY_TOKEN
SSH_HOST Hôte du VPS
SSH_USER Utilisateur SSH
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

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)

mkdir -p /opt/benevoles
cd /opt/benevoles
cat > .env <<'EOF'
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-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 — un jeton de lien magique en clair dans les logs du
# conteneur permet de se connecter à la place de n'importe qui, donc à
# repasser à true avant l'ouverture au public (le backend affiche un
# avertissement au démarrage tant que c'est le cas).
EMAIL_ENABLED=true
SMTP_HOST=smtp.mon-domaine.fr
SMTP_PORT=587
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-domaine.fr>
EOF

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 Gitea, lancer le pipeline sur main normalement (Push → Actions, 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

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 :

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 Gitea, ajouter la variable VITE_EDITION_ID (Paramètres → Actions → Variables) avec cet UUID, puis relancer le pipeline sur main (bouton Re-run sur le dernier run, 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 à son Caddyfile (ou au fichier qu'il importe pour les sites), avec le vrai nom de domaine, puis :

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.