Production : un VPS OVHcloud (VPS-3), Ubuntu 26.04, Docker Compose, depuis le 7 octobre 2026. Pare-feu ufw (SSH,
80, 443), fail2ban, unattended-upgrades. Le serveur n'a que le dossier ~/slidz/deploy (dépôt slidzapp/deploy).
Services et domaines : Architecture.
git push main (back | admin | site)
└─ GitHub Actions
├─ back : pytest -q ─┐
├─ admin : npm ci && npm run build ├─ échec → rien n'est publié
└─ site : vérification des liens ─┘
└─ job publish : docker build + push
ghcr.io/slidzapp/<dépôt>:latest
ghcr.io/slidzapp/<dépôt>:<sha du commit>
serveur (à la main) :
cd ~/slidz/deploy && docker compose pull && docker compose up -d
└─ api : alembic upgrade head && uvicorn --workers ${API_WORKERS:-4}
pull est manuel.docker compose up -d ne recrée que les conteneurs dont l'image ou la configuration a changé.uvicorn ne démarre pas, le conteneur redémarre en boucle (restart: unless-stopped).docker compose logs api.pg_advisory_lock(734001)) : seed, thèmes, badges, saison, ADMIN_PSEUDOS.curl https://api.slidzapp.com/health puis /config.SLIDZ_TAG=<sha> dans .env, puis la même commande. La variable s'applique aux trois images api, admin, site.
| Quoi | Mécanisme |
|---|---|
postgres |
healthcheck Compose pg_isready -U slidz (5 s, timeout 3 s, 20 essais) ; api et wiki attendent service_healthy |
redis |
aucun healthcheck ; api attend seulement service_started |
api, admin, site, wiki, caddy |
aucun healthcheck Compose |
GET /health |
SELECT 1 sur PostgreSQL + PING Redis ; 200 {"status":"ok"} ou 503 {"status":"degraded","db":…,"redis":…} (back/app/main.py, health) |
| Sondes externes | Uptime Kuma sur /health, le site, la console |
| Machine | Beszel (docker-compose.monitoring.yml) : tableau de bord sur 127.0.0.1:8090 (tunnel SSH), agent par socket Unix (profil agent), variables BESZEL_KEY, BESZEL_TOKEN |
backup)Image postgres:16-alpine, boucle sh dans deploy/docker-compose.yml. Chaque tour :
sleep jusqu'à « tomorrow 03:00 » (sinon +86400 s)
pg_dump -h postgres -U slidz slidz | gzip > /backups/slidz-AAAA-MM-JJ.sql.gz
pg_dump -h postgres -U slidz wiki | gzip > /backups/wiki-AAAA-MM-JJ.sql.gz
tar -czf /backups/avatars-AAAA-MM-JJ.tar.gz -C /media avatars (volume media en lecture seule)
find /backups -name '<préfixe>-*' -mtime +14 -delete
| Point | Valeur |
|---|---|
| Destination | deploy/backups/ (hors git) |
| Rétention | -mtime +14 : fichiers de plus de 14 jours entiers supprimés (environ 15 jours gardés) |
| Format | SQL texte (pg_dump sans --clean), gzip |
| Mot de passe | PGPASSWORD = POSTGRES_PASSWORD |
| Hors serveur | sauvegarde quotidienne OVHcloud de la machine ; copie de backups/ vers un stockage objet demandée par DEPLOY.md, pas automatisée |
| Redis | non sauvegardé (AOF dans redis_data) ; tout s'y reconstruit depuis PostgreSQL |
Le dump ne supprime rien : restaurer dans une base vide.
cd ~/slidz/deploy
docker compose stop api wiki
docker compose exec postgres dropdb -U slidz slidz && docker compose exec postgres createdb -U slidz slidz
gunzip -c backups/slidz-AAAA-MM-JJ.sql.gz | docker compose exec -T postgres psql -U slidz slidz
# wiki : même chose avec la base wiki et backups/wiki-AAAA-MM-JJ.sql.gz
# avatars (nom du volume : docker volume ls, en général deploy_media) :
docker run --rm -v deploy_media:/media -v "$PWD/backups:/b:ro" alpine tar -xzf /b/avatars-AAAA-MM-JJ.tar.gz -C /media
docker compose up -d
Après restauration, Redis garde d'anciens classements : docker compose exec redis redis-cli --scan --pattern 'lb:*'
puis les supprimer ; ils se reconstruisent à la première lecture.
deploy/Caddyfile monté en lecture seule ; certificats et comptes ACME dans le volume caddy_data (à ne pasemail.api, console, wiki ; X-Frame-Options DENY sur console ; nosniff sur les trois.encode gzip partout. www → redirection 301.Caddyfile n'est pas pris par docker compose up -d (fichier monté, conteneur inchangé) :docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile (--force pour relancer une demande de/api/, /auth/, /avatars/ vers api:8000 avec le DNS de Docker revalidé toutes lesapi recréé ne donne pas de 502 durable.wiki du même PostgreSQL (sauvegardée avec l'autre).git@github.com:slidzapp/wiki.git, branche main, mode/wiki/git-key/id_ed25519 depuis secrets/wiki_git/.slidzapp/wiki seulement.127.0.0.1:3000, avant de créer le DNS (le premier visiteur| Règle | Pourquoi |
|---|---|
rsync du dossier deploy toujours avec --exclude .env |
ne jamais écraser ni copier le .env du serveur |
.env, secrets/, backups/ dans deploy/.gitignore |
jamais committés |
Secrets en fichiers (secrets/firebase.json, clé Sign in with Apple .p8, secrets/wiki_git/) déposés à la main, chmod 600 |
ni dans git, ni dans .env |
Variables secrètes (noms) : POSTGRES_PASSWORD, JWT_SECRET, APP_ACCOUNT_PASSWORD, ANTHROPIC_API_KEY, ANTHROPIC_WORKSPACE_ID, BESZEL_KEY, BESZEL_TOKEN |
dans .env du serveur seulement |
docker login ghcr.io fait par Lucas (jeton read:packages) |
les images sont privées |
ADS_TRUST_CLIENT jamais à vrai en production |
sinon l'app crédite des pubs sans Google |
Seul caddy publie des ports |
api, admin, site, wiki en expose uniquement ; Beszel sur 127.0.0.1 |
Console réservée à is_admin (deps.admin_user) ; wiki : comptes Wiki.js |
aucune page interne sans compte |
date -d "tomorrow 03:00" échoue avec le date de busybox de postgres:16-alpinenow + 86400 : la sauvegarde part 24 h après le démarrage dubackup, puis toutes les 24 h, en UTC, et pas à 3 h. Chaque recréation du conteneur décale le cycle.set -o pipefail : si pg_dump échoue, un fichier .sql.gz presque vide est écrit quand même.SLIDZ_TAG est commun aux images back, admin et site, mais chaque CI tague avec le sha de son dépôt :pull en échec).CLAUDE.md dit la production servie par le serveur maison (ssh prod) : c'est le VPS OVHcloud ; le serveur maisonssh prod de DEPLOY.md (section serveur maison, notifications push) visent l'ancien serveur.DEPLOY.md (§ Comptes) dit que seul le premier pseudo d'ADMIN_PSEUDOS devient admin : le code promeut tousmain.py, lifespan).DEPLOY.md (§ Plusieurs processus API) parle de « duels » entre workers : il n'y a plus de duel face à face, ce sont/ws qui passent par Redis.secrets/ est monté dans api (/run/secrets/slidz), y compris wiki_git/ dont l'API n'a pas besoin.api : docker compose ps affiche « running » même si /health répond 503.Code : deploy/docker-compose.yml (services api, backup, postgres), deploy/Caddyfile,
deploy/docker-compose.monitoring.yml, deploy/DEPLOY.md, deploy/.gitignore, back/app/main.py (health,
lifespan), admin/nginx.conf.template, back/.github/workflows/ci.yml, admin/.github/workflows/ci.yml,
site/.github/workflows/ci.yml