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éeracapbe-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 estcapbe.ovh. Les apps vivent sous le sous-domaineci(recordsA 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.ovhré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-*.rpmtéléchargé depuis oracle.com/database/free (compte gratuit) → placé dansplaybook/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é dansconfig/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+autodiscover→ IP 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 zonecapbe.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) ouscp
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 list → capbe-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 list → capbe-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/shmrequis) : 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-nrdans 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 auincus 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 → caddyest 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 (objetCertificate+SystemSettings.defaultCertificateId). Pièges constatés en réel : (1) le service tourne enUser=stalwart— un privkey en 600 root est illisible → échec silencieux et repli sur l'auto-signé « rcgen » (le script fait lechown 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 zonecapbe.ovhtout 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 viastalwart-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 publientv=spf1 mx ip4:<egress> -all. Vérifier :dig +short TXT capbe.ovhdoit contenirip4:. 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=ysur 23ai Free peut se bloquer longuement au pointNORMAL_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 passestatistics=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îtreSYS_EXPORT_FULL_01qui 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-gettimeout dans les conteneurs alors que le ping passe. Correction root :iptables -I FORWARD 1 -i capbe-br0 -j ACCEPT(et-o capbe-br0, idemincusbr0). 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-egressSans persistance, l'étape 03 bascule en mode hors-ligne (Caddy en.debpoussé 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-ownerappartiennent àpostgres— le script réattribue la propriété (schémas utilisateur, tables, vues, séquences, fonctions) au rôle cible (app/odoo) viafix_owner+sweep_owner(DO blocks ;REASSIGN OWNEDéchoue sur PG15+). ⚠️ Le sweep est indispensable : les tablesorm_signaling_*(Odoo 19) restaurées possédées parpostgressont invisibles au rôleodoodansinformation_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/htmlou/usr/share/caddy(racine Caddy — cas agroworoba) ; API détectée dans/opt/<legacy>/apiou/app. Si la base Odoo source n'a jamais été initialisée (pas deir_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) ; relancer09-caddy-config.shaprès la migration pour activer le routage (blocsmeca.app.ci.capbe.ovh→ pgis:8000 /agro.app.ci.capbe.ovh→ pgis:8001 déjà dansconfig/Caddyfile). - Rewrite API
/api/*→/api/v2/*(09) : les blocsapi.ci.capbe.ovhet<id>.app.ci.capbe.ovhréécrivent vers/api/v2/uniquement les ressources listées dans la regexpath_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_tlsexige un endpoint « ask » (interval/burstsupprimés) — le Caddyfile complet déclareon_demand_tls { ask http://localhost:8080/check }avec un bloc:8080local qui autorise uniquement les hôtes*.capbe.ovh(matcher expression, testé à l'exécution). Ne pas réintroduireon_demand_tls {}ouon_demand_tlsseul : 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.