7.7 KiB
Déploiement
Le pipeline .gitea/workflows/ci-cd.yml (Gitea
Actions) se déclenche sur chaque push :
- tests (backend :
ruff+pytest+alembic check; frontend :typecheck+lint+build+smoke) ; - sur
mainseulement : build des imagesbackendetfrontend, poussées sur le registre de conteneurs intégré à Gitea ; - copie de
deploy/docker-compose.prod.ymlsur le VPS par SSH ; 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 enENVIRONMENT=productionsinon (app/config.py:check_production_secrets).EMAIL_ENABLED=trueavec 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 viapython -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/loginet/auth/magic-linksont limités en tentatives par email (5/min et 3/5 min) — voirapp/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.
- Sur Gitea, lancer le pipeline sur
mainnormalement (Push → Actions, ou un simple commit). Le jobbuild-frontendéchouera ou produira une image sans édition tant queVITE_EDITION_IDn'existe pas : c'est attendu, on l'ignore pour l'instant. - Une fois
build-backendetdeploypassés,dbetbackendtournent 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.