Checklist de déploiement — CAPBE sur serveur OVH

📄 Source : playbook/DEPLOY.md 🕒 Généré le 06/08/2026 09:56
Sommaire

Checklist pas-à-pas pour déployer l'infrastructure CAPBE avec le playbook Incus. Durée totale estimée : 45–60 min (dont 20–30 min pour Oracle).


0. Prérequis (avant de commencer)

  • [ ] Serveur dédié OVH (root), Ubuntu 24.04 ou compatible
  • [ ] Incus installé et initialisé : incus admin init (réseau par défaut ok, on créera capbe-br0 à l'étape 01) — vérifier : incus version
  • [ ] DNS OVH pointant vers l'adresse d'écoute du forward : ci.capbe.ovh, *.ci.capbe.ovh, *.app.ci.capbe.ovh ⚠️ La zone DNS chez OVH est capbe.ovh. Les apps vivent sous le sous-domaine ci (records A ci, A *.ci, A *.app.ci) ; la MESSAGERIE est au contraire à la RACINE de la zone (domaine mail = capbe.ovh : A mail, A webmail, A autoconfig, A autodiscover, MX/TXT/_dmarc/SRV sans préfixe). L'API /domain/zone/ci.capbe.ovh répond 404 « service does not exist » ; il faut /domain/zone/capbe.ovh — constaté en réel.
  • [ ] Aucune IP failover requise : le forward public (80/443 → caddy) est créé manuellement dans la web UI Incus après l'étape 03 (le playbook vérifie qu'il est en place avant de poursuivre)
  • [ ] RPM oracle-database-free-*.rpm téléchargé depuis oracle.com/database/free (compte gratuit) → placé dans playbook/downloads/
  • [ ] Messagerie : jeton API OVH (OVH_AK/OVH_AS/OVH_CK) créé sur eu.api.ovh.com/createToken (GET/PUT/POST/DELETE /domain/zone/*) et renseigné dans config/secrets.env → publication des records : sudo ./publish-mail-dns.sh (ou étape 07)
  • [ ] Port 25 sortant débloqué pour l'IP d'égresse du serveur mail (IP dédiée si présente, sinon IP principale de l'hôte — espace client OVH)
  • [ ] Records DNS messagerie (A mail.capbe.ovh + webmail + autoconfig + autodiscoverIP dédiée du serveur mail (NIC routée Incus, ex. 51.79.11.52) quand présente, sinon IP du forward ; MX, SPF, DKIM, DMARC, PTR, SRV _imap._tcp → 993 + _submission._tcp → 587 + _pop3._tcp → 995 + _autodiscover._tcp → 443) — à la RACINE de la zone capbe.ovh — créés par l'étape 07 si le jeton le permet, sinon à publier à la main
  • [ ] Playbook copié sur le serveur : git clone (dépôt local) ou scp

Contraintes garanties par le playbook

  • Aucun port de l'hôte : exposition uniquement via network forward Incus (DNAT noyau, rien dans ss -tln).
  • Aucun conteneur Docker : le playbook ne crée jamais de conteneur Docker (Incus uniquement). Docker peut rester installé et actif pour d'autres projets — le préflight (00) émet un avertissement, pas un arrêt.

1. Préparation

cd playbook
chmod +x *.sh lib/common.sh

2. Exécution étape par étape

# Commande Durée Vérification
00 sudo ./00-preflight.sh ~1 min « Secrets prêts » ; ls -l config/secrets.env-rw------- (600)
01 sudo ./01-network.sh ~1 min incus network show capbe-br0 (10.10.10.1/24, NAT)
02 sudo ./02-profiles.sh ~1 min incus profile list → pgis/oracle/odoo/front/caddy
03 sudo ./03-caddy.sh ~2 min conteneur caddy + Caddyfile minimal ; ⏸️ pause : crée le forward MANUEL 80/443 → caddy dans la web UI Incus puis Entrée ; vérifié sinon arrêt
04 sudo ./04-pgis.sh 5–8 min incus exec capbe-pgis -- su postgres -c "psql -d gisdb -c 'SELECT postgis_version();'"
05 sudo ./05-oracle.sh 20–30 min incus exec capbe-oracle -- ps -ef \| grep -E 'ora_\|tnslsnr' (⚠ RPM obligatoire dans downloads/)
06 sudo ./06-odoo.sh ~10 min incus exec capbe-odoo -- ss -tlnp \| grep 8069
sudo ./check-mail-prereqs.sh (optionnel) ~10 s sortie 0 = prêt, 1 = bloquant(s), 2 = avertissement(s)
07 sudo ./07-mail.sh 15–20 min incus exec capbe-mail -- ss -tlnp (25/110/143/465/587/993/995/4190) ; webmail https://webmail.capbe.ovh + smoke-test autoconfig/autodiscover ; records DNS publiés ; tout le mail exposé via l'IP dédiée de capbe-mail (aucun port mail dans le forward)
08 sudo ./08-frontend.sh projetA 2–3 min incus listcapbe-projetA-front présent (répéter par projet)
09 sudo ./09-caddy-config.sh ~2 min incus exec capbe-caddy -- caddy validate --config /etc/caddy/Caddyfile ; curl -I https://ci.capbe.ovh (TLS) ; liveness :80/:443 vérifiée
10 sudo ./10-backup.sh --cron ~1 min crontab -l → ligne 10-backup.sh à 02h00
sudo ./smoke-test.sh (optionnel, post-09) ~1 min sortie 0 = tout OK ; vérifie rendu webmail, JMAP, SRV, conformité hôte
12 sudo ./12-migrate-projects.sh agrimeca=meca agroworoba=agro (optionnel) 10–15 min incus listcapbe-meca/capbe-agro ; incus exec capbe-pgis -- systemctl is-active meca-api ; tenant Odoo https://meca.ci.capbe.ovh

3. Pièges connus

  • Oracle (05) : le RPM est téléchargé à la main — sans lui, le script s'arrête proprement avec un message. Le conteneur est en mode privilégié (/dev/shm requis) : choix assumé pour un conteneur isolé. Si l'installation bloque sur /dev/shm, bascule possible en VM Incus.
  • Sysctls Oracle (02/05) : imposer linux.sysctl.fs.aio-max-nr dans le profil d'un conteneur privilégié fait échouer le boot LXC en « Read-only file system » (constaté sur Ubuntu 24.04 / kernel 6.8 — le sysctl est global, non namespaced) → l'étape 05 s'arrête au incus launch. L'étape 02 ne pose les sysctls (shmmax/shmall/sem/aio-max-nr) que si l'hôte est sous le minimum Oracle ; dans le cas nominal l'hôte les couvre déjà (aio-max-nr ≥ 1 M, shmmax/shmall énormes) et le profil n'impose rien.
  • Exposition (03 — forward MANUEL) : le playbook ne crée jamais le forward — il est créé dans la web UI Incus (Network → capbe-br0 → Forwards), adresse d'écoute libre (aucune IP failover requise). Le script fait une ⏸️ pause interactive pour le créer, puis vérifie que 80/443 → caddy est en place avant de poursuivre (sinon arrêt avec instructions). Depuis la bascule « 1 IP » (08/2026), aucun port mail ne fait partie du forward : capbe-mail expose tout (webmail + protocoles) sur son IP dédiée (NIC routée Incus) — le forward garde uniquement 80/443 → caddy (apps web). Si un service tiers écoute déjà sur 80/443 sur l'hôte, un avertissement est émis.
  • Multi-tenant Odoo : un tenant = une base ; dbfilter ^%d$ route par sous-domaine, list_db=False (ne pas réactiver).
  • Messagerie (07) : le jeton API OVH est requis (DNS-01 + records) ; si le bootstrap automatique de Stalwart échoue, termine l'assistant à la main (incus exec capbe-mail -- curl http://127.0.0.1:8080/admin) ; le certificat TLS wildcard émis dans /etc/stalwart/tls/ est associé automatiquement dans Stalwart (objet Certificate + SystemSettings.defaultCertificateId). Pièges constatés en réel : (1) le service tourne en User=stalwart — un privkey en 600 root est illisible → échec silencieux et repli sur l'auto-signé « rcgen » (le script fait le chown stalwart:stalwart) ; (2) l'id de l'objet Certificate est auto-assigné par le serveur (le poser ensuite comme certificat par défaut) ; (3) le DNS-01 acme.sh trouve la zone capbe.ovh tout seul en remontant les labels. Débloque le port 25 sortant et vérifie le PTR avant d'envoyer ; le webmail SnappyMail est durci automatiquement (IMAP localhost:993 SSL + SMTP localhost:587 STARTTLS — l'assistant écrit du plain 143/25) ; le PTR exige un jeton avec droits /ip/ (sinon espace client OVH) ; la sauvegarde (étape 10) utilise le mot de passe admin courant — à adapter si tu le changes dans la WebUI. POP3 n'est pas actif par défaut dans Stalwart* — le script l'active via stalwart-cli (repli : admin Stalwart → Settings → Listeners : pop3 sur 110, pop3s sur 995).
  • SPF : l'IP d'égresse (sortante) doit être autorisée — « v=spf1 mx -all » n'autorise QUE l'IP du MX alors que les connexions SORTANTES partent de l'IP d'égresse. Depuis la bascule « 1 IP » (08/2026), l'égresse est l'IP DÉDIÉE du serveur mail (NIC routée Incus, ex. 51.79.11.52 — PTR propre, réputation séparée du trafic web). Symptôme constaté en réel : mail-tester.com rejette l'envoi en 550 5.7.23 SPF fail (RCPT) tant que l'égresse n'est pas dans le SPF. Les scripts 07/publish-mail-dns détectent l'IP sortante (incus exec capbe-mail -- curl ifconfig.me) et publient v=spf1 mx ip4:<egress> -all. Vérifier : dig +short TXT capbe.ovh doit contenir ip4:. PTR : l'IP à faire pointer est l'IP SORTANTE (egress — celle que les récepteurs voient) → mail.capbe.ovh (FCrDNS : le hostname doit résoudre vers l'IP) — jeton /ip/* requis (sinon espace client OVH → onglet IP → Configurer le reverse → mail.capbe.ovh).
  • Sauvegardes (10) — expdp Oracle : expdp full=y sur 23ai Free peut se bloquer longuement au point NORMAL_OPTIONS/TABLE (80 Ko figés, jusqu'à 30+ min) à cause du calcul des statistiques des grosses tables système (IDL_UB1$ ~370 Mo) — le script passe statistics=none (dump complet et restaurable ; les stats se régénèrent à la restauration). Autres protections : job_name=CAPBE_<TS> unique + reuse_dumpfiles=y (un expdp tué laisse une table maître SYS_EXPORT_FULL_01 qui bloque le job suivant)
  • purge des expdp orphelins + garde flock (deux runs simultanés — cron 02:00 + run manuel — collidaient sur le job Data Pump). Le run complet dure ~10-15 min (l'export Oracle domine).
  • Egress des conteneurs (firewall hôte) : sur certains serveurs, le TCP sortant des conteneurs (capbe-br0, incusbr0) est droppé par le firewall de l'hôte (règles FORWARD posées par Docker). Symptôme : apt-get timeout dans les conteneurs alors que le ping passe. Correction root : iptables -I FORWARD 1 -i capbe-br0 -j ACCEPT (et -o capbe-br0, idem incusbr0). Persistance au reboot — installer l'unit fournie : bash sudo cp playbook/config/capbe-egress.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now capbe-egress Sans persistance, l'étape 03 bascule en mode hors-ligne (Caddy en .deb poussé depuis l'hôte) mais les étapes 04→07 restent bloquées après reboot.
  • Migration de projets (12) : les objets restaurés via pg_dump --no-owner appartiennent à postgres — le script réattribue la propriété (schémas utilisateur, tables, vues, séquences, fonctions) au rôle cible (app/odoo) via fix_owner + sweep_owner (DO blocks ; REASSIGN OWNED échoue sur PG15+). ⚠️ Le sweep est indispensable : les tables orm_signaling_* (Odoo 19) restaurées possédées par postgres sont invisibles au rôle odoo dans information_schema → Odoo tente de les recréer (DuplicateTable) → registre KO (KeyError: '<db>', login 500 — constaté en réel sur meca). Le filestore Odoo (pièces jointes) n'est pas dans le dump : copie automatique si un chemin standard existe, sinon avertissement. Front détecté dans /var/www/html ou /usr/share/caddy (racine Caddy — cas agroworoba) ; API détectée dans /opt/<legacy>/api ou /app. Si la base Odoo source n'a jamais été initialisée (pas de ir_module_module), le script crée un tenant vierge (odoo -i base). API : un port par service uvicorn (8000 pour la 1re, 8001 pour la suivante) ; relancer 09-caddy-config.sh après la migration pour activer le routage (blocs meca.app.ci.capbe.ovh → pgis:8000 / agro.app.ci.capbe.ovh → pgis:8001 déjà dans config/Caddyfile).
  • Rewrite API /api/*/api/v2/* (09) : les blocs api.ci.capbe.ovh et <id>.app.ci.capbe.ovh réécrivent vers /api/v2/ uniquement les ressources listées dans la regex path_regexp (health, dashboard, clients…). Tout nouvel endpoint d'une API migrée doit être ajouté à cette regex, sinon il part non-réécrit vers pgis et répond 404 (le backend sert /api/v2/*).
  • On-Demand TLS (09) : depuis Caddy ≥ 2.9 (v2.11 ici), on_demand_tls exige un endpoint « ask » (interval/burst supprimés) — le Caddyfile complet déclare on_demand_tls { ask http://localhost:8080/check } avec un bloc :8080 local qui autorise uniquement les hôtes *.capbe.ovh (matcher expression, testé à l'exécution). Ne pas réintroduire on_demand_tls {} ou on_demand_tls seul : validation KO.
  • Idempotence : tous les scripts tolèrent la re-exécution (warn si déjà fait).
  • Secrets : config/secrets.env (chmod 600) n'est jamais commité (.gitignore + hook pre-commit anti-secrets).

4. Validation finale

curl -I https://ci.capbe.ovh            # hub CAPBE (200)
curl -I https://odoo.ci.capbe.ovh       # Odoo (multi-tenant)
curl -I https://api.ci.capbe.ovh        # FastAPI Stack PostgreSQL
curl -I https://oracle-api.ci.capbe.ovh # FastAPI Stack Oracle
curl -I https://projetA.app.ci.capbe.ovh # frontend projetA
# Messagerie — vérification du CONTENU rendu (pas seulement le statut HTTP)
curl -fsS https://webmail.capbe.ovh/ | grep -qiE 'snappy|<html' && echo 'webmail OK (PHP rendu)' || echo 'webmail KO (PHP non rendu — voir logs Caddy)'
# NB : le shell HTML peut être rendu même si l'app ne démarre pas. Le bundle JS
# doit être servi AVEC contenu (sinon écran figé « SnappyMail » — expansion
# php_fastcgi Caddy ≥ 2.9 sans file_server ⇒ fichiers statiques servis VIDE) :
VER=$(curl -fsS https://webmail.capbe.ovh/ | grep -oE '/snappymail/v/[0-9.]+/' | head -1)
[ -n "$VER" ] && curl -fsS "https://webmail.capbe.ovh${VER}static/js/min/app.min.js" | wc -c \
  || echo 'version SnappyMail non détectée'  # attendu : ~200000 octets (non 0)
curl -fsS https://mail.capbe.ovh/.well-known/jmap | grep -q '"apiUrl"' && echo 'JMAP OK (session Stalwart)' || echo 'JMAP KO (endpoint Stalwart injoignable)'
curl -I https://autoconfig.capbe.ovh/mail/config-v1.1.xml      # autoconfig (Thunderbird) → 200
curl -I https://webmail.capbe.ovh/.well-known/autoconfig/mail/config-v1.1.xml  # repli well-known → 200
curl -I https://autodiscover.capbe.ovh/autodiscover/autodiscover.xml  # autodiscover (mobiles/Outlook) → 200
# Records SRV (RFC 6186 + autodiscover Outlook) — si dig est installé (dnsutils)
dig +short SRV _imap._tcp.capbe.ovh        # attendu : 0 0 993 mail.capbe.ovh.
dig +short SRV _submission._tcp.capbe.ovh   # attendu : 0 0 587 mail.capbe.ovh.
dig +short SRV _pop3._tcp.capbe.ovh         # attendu : 0 0 995 mail.capbe.ovh.
dig +short SRV _autodiscover._tcp.capbe.ovh # attendu : 0 0 443 autodiscover.capbe.ovh.
dig +short MX capbe.ovh                     # attendu : 10 mail.capbe.ovh.
dig +short TXT capbe.ovh                    # attendu : "v=spf1 mx ip4:<egress> -all"
dig +short TXT _dmarc.capbe.ovh             # attendu : "v=DMARC1; p=quarantine; ..."
dig +short TXT v1-rsa-20260802._domainkey.capbe.ovh  # DKIM rsa (sélecteurs Stalwart)
sudo ./10-backup.sh                     # lancement manuel d'une sauvegarde
# PTR (reverse DNS) : 1 seule IP à faire pointer — l'IP SORTANTE/egress
# (déduite du SPF ip4:, ex. 51.79.11.52) → mail.capbe.ovh — procédure OVH
# détaillée dans POST-DEPLOY.md ; vérifié par le smoke-test
# Audit de délivrabilité rejouable (4 comptes + sonde MX + chaîne DNS) :
sudo ./audit-deliverability.sh            # cible interne — aucun mail externe
#   … ou avec une boîte externe pour valider côté récepteur (SPF/DKIM/DMARC PASS)

Vérification de conformité finale :

ss -tln | grep -E ':(80|443)\s'   # ne doit rien afficher (aucun port hôte)

5. Rollback / teardown

sudo ./11-teardown.sh — détruit toute l'infrastructure CAPBE (forward public, conteneurs, profils, réseau capbe-br0, cron des sauvegardes) après confirmation OUI. Conserve les sauvegardes (/var/backups/capbe) et les secrets (config/secrets.env) ; ajouter --purge-secrets pour tout effacer. Un re-déploiement complet reste possible en relançant les étapes 00→10.


6. Audit qualité (optionnel)

# Depuis la racine du dépôt (les directives `# shellcheck source=` pointent
# vers playbook/lib/common.sh — chemin relatif au répertoire de lancement) :
shellcheck -x -S warning playbook/*.sh playbook/lib/common.sh .githooks/pre-commit

# Tests unitaires rejouables (sans incus ni root) :
#   - filtre awk du Caddyfile (blocs meca/agro) + rotation des sauvegardes
./playbook/tests/test-filters.sh    # attendu : « Bilan : 11 OK / 0 échec(s) »
#   - lib/mail.sh send_alert_mail : contrat (MAIL_OK/MAIL_FAIL, mdp jamais en
#     argv, round-trip base64, ANSI, retours 1, SECRETS_FILE) — incus stubbé,
#     aucun envoi réel
./playbook/tests/test-mail-lib.sh   # attendu : « Bilan : 13 OK / 0 échec(s) »
#   - lib/common.sh detect_mail_a_ip : IP A mail.<domaine> (dédiée vs forward),
#     incus stubbé — utilisé par publish-mail-dns.sh / 07-mail.sh / smoke-test.sh
./playbook/tests/test-mail-dns.sh   # attendu : « Bilan : 8 OK / 0 échec(s) »

# Audit de délivrabilité rejouable (auth SMTP 587 + envoi + sonde MX Gmail +
# chaîne DNS pour les 4 comptes) — usage : voir POST-DEPLOY.md §4c
sudo ./playbook/audit-deliverability.sh

bash -n sur chaque script : for f in playbook/*.sh; do bash -n "$f"; done.