Architecture¶
Serveur¶
| Champ | Valeur |
|---|---|
| Hôte | Kimsufi OVH |
| FQDN | ns3112907.ip-54-37-255.eu |
| IPv4 | 54.37.255.22 |
| OS | Debian 13 |
| Docker | 29.x |
| Specs | 32 Go RAM, 108 Go disque |
| SSH | deploy@, port 2222 (le port 22 est pris par Gitea pour les push git) |
| Clé SSH | ~/.ssh/id_ed25519 (avec passphrase, à charger dans ssh-agent) |
fail2ban tourne en plus côté hôte pour SSH (coexiste avec CrowdSec).
Réseaux Docker¶
| Réseau | Type | Usage |
|---|---|---|
proxy_net | external | Le réseau public de Caddy. Tout service web s'y connecte pour être proxifié. |
core_internal_net | external | Réseau interne pour les services core (gitea ↔ gitea_db, etc.). |
vaultwarden_internal | stack | Vaultwarden ↔ son Postgres. |
teslamate_internal | stack | Teslamate ↔ db ↔ mosquitto. |
topfrag_internal | stack | Topfrag ↔ son db. |
Règle d'or : les bases de données ne doivent jamais être sur proxy_net.
Reverse proxy : Caddy + CrowdSec¶
- Image :
caddy-crowdsec:local(build custom, Caddy + caddy-docker-proxy + bouncer hslatman/crowdsec). - Compose :
/srv/docker/caddy/docker-compose.ymlsur l'hôte. - Mode : caddy-docker-proxy lit les labels Docker des conteneurs sur
proxy_netpour générer le Caddyfile dynamiquement (pas de Caddyfile statique à maintenir). - Ports exposés : 80, 443, 443/udp.
- Logs :
/srv/docker/caddy/logs/access.log(logs opérationnels + accès, format JSON).
Pattern de labels d'un service web¶
labels:
caddy: monservice.carai.be
caddy.reverse_proxy: monservice:8080 # ← nom du conteneur, PAS {{upstreams 8080}}
caddy.import: secure_headers
caddy.crowdsec: ""
caddy.log: ""
Important : utiliser le nom du conteneur comme upstream, pas le template {{upstreams PORT}}. Le template injecte l'IP du conteneur en dur au moment de la génération du Caddyfile ; quand le conteneur est recréé (nouvelle IP), Caddy garde l'ancienne IP en cache → 502, et il faut docker restart caddy pour rafraîchir. Avec le nom de conteneur, Caddy résout via le DNS Docker à chaque requête → toujours l'IP courante, aucune intervention requise après recreation.
Gotcha connu¶
Quand on déploie plusieurs stacks à la suite + restaure des données (cycles stop/start rapprochés), caddy-docker-proxy peut se désynchroniser : un site est dans le Caddyfile généré mais pas dans la config active de Caddy → pas de cert, HTTP 000. Fix : docker restart caddy (re-scan complet, ~5s de coupure, les certs déjà émis sont en cache).
Inventaire des stacks¶
Core (CLI compose, sous /srv/docker/)¶
Pas géré par Portainer — docker compose up -d à la main, configuration en local sur le serveur.
| Stack | Conteneurs | Image | Sous-domaine |
|---|---|---|---|
| caddy | caddy | caddy-crowdsec:local | (aucun — c'est le proxy) |
| crowdsec | crowdsec | crowdsecurity/crowdsec:latest | (interne) |
| gitea | gitea, gitea_db | gitea/gitea:1-rootless, postgres:16-alpine | gitea.carai.be |
| portainer | portainer | portainer/portainer-ce:latest | portainer1.carai.be |
Stacks Portainer (déployées « from Git » depuis gitea.carai.be/phantoms/)¶
| Id | Stack | Conteneurs | Sous-domaine | Repo Gitea |
|---|---|---|---|---|
| 6 | topfrag | topfrag-frontend-1, topfrag-backend, topfrag-db-1 | topfrag.carai.be, api.topfrag.carai.be | (pré-existant) |
| 7 | vaultwarden | vaultwarden, vaultwarden_db | vault.carai.be | phantoms/vaultwarden |
| 8 | budget-commun | budget-commun | budget.carai.be | phantoms/budget-commun |
| 9 | budget-yanis | budget-yanis | budget-yanis.carai.be | phantoms/budget-yanis |
| 10 | budget-carhene | budget-carhene | budget1.carai.be | phantoms/budget-carhene |
| 11 | budget-fred | budget-fred | budget-fred.carai.be | phantoms/budget-fred |
| 12 | budget-lise | budget-lise | budget-lise.carai.be | phantoms/budget-lise |
| 13 | teslamate | teslamate, teslamate_db, teslamate_grafana, teslamate_mosquitto, teslamate_nodered | teslamate.carai.be, dashboard.carai.be, nodered.carai.be | phantoms/teslamate |
| 14 | gm2-qr | gm2-qr (image buildée par Portainer) | basic-fit.carai.be | phantoms/gm2-qr |
| 15 | uptime-kuma | uptime-kuma | uptime.carai.be | phantoms/uptime-kuma |
| 16 | healthchecks | healthchecks | healthchecks.carai.be | phantoms/healthchecks |
| 17 | backrest | backrest | backrest.carai.be | phantoms/backrest |
| 18 | pg-dumper | pg-dumper (image buildée par Portainer) | (interne) | phantoms/pg-dumper |
| 19 | watchtower | watchtower | (interne) | phantoms/watchtower |
Total : 26 conteneurs, 17 sous-domaines publics.
DNS — zone carai.be chez OVH¶
Tous les sous-domaines applicatifs sont des CNAME → carai.be, et carai.be est un A → 54.37.255.22. Donc pour ajouter un nouveau sous-domaine, il suffit de créer un CNAME.
Sous-domaines actuellement utilisés : vault, gitea, portainer1, topfrag, api.topfrag, budget, budget-yanis, budget1, budget-fred, budget-lise, teslamate, dashboard, nodered, basic-fit, uptime, healthchecks, backrest.
Schéma général¶
flowchart LR
Internet([Internet]) -- "80/443" --> Caddy
Caddy <--> CrowdSec
Caddy --> Vault[vault]
Caddy --> Budgets[budget*]
Caddy --> Teslamate[teslamate / dashboard / nodered]
Caddy --> GM2[basic-fit]
Caddy --> Uptime[uptime]
Caddy --> HC[healthchecks]
Caddy --> Backrest
Caddy --> Gitea
Caddy --> Portainer
Caddy --> Topfrag
subgraph "Bases internes"
Vault --- VaultDB[(vaultwarden_db)]
Teslamate --- TmDB[(teslamate_db)]
Teslamate --- Mqtt[mosquitto]
Gitea --- GiteaDB[(gitea_db)]
Topfrag --- TopDB[(topfrag-db-1)]
end
subgraph "Pipeline backup"
PgDumper[pg-dumper] --> PgDumps[(volume pg_dumps)]
end
PgDumper -. "docker exec pg_dumpall" .-> VaultDB
PgDumper -.-> TmDB
PgDumper -.-> GiteaDB
PgDumper -.-> TopDB
Backrest -- "restic + rclone" --> GDrive[(Google Drive)]
Backrest -. "lit" .-> Volumes[(volumes Docker)]
Backrest -. "lit" .-> SrvDocker[(/srv/docker)]
Backrest -. "lit" .-> PgDumps Workspace local (Mac)¶
~/kluedo-rescue/migration/<service>/ pour chaque service migré : - docker-compose.yml - README.md - .env (secrets, gitignored) - .gitignore - éventuel script de restauration / utilitaires
Ces dossiers sont chacun un repo git poussé sur gitea.carai.be/phantoms/<service>.
Hygiène / maintenance¶
Auto-update Docker — Watchtower¶
Stack watchtower (Id 19), schedule lundi 04h00 (Europe/Paris), notifications Discord. Mode opt-in par label : WATCHTOWER_LABEL_ENABLE=true. Watchtower n'update QUE les conteneurs portant le label com.centurylinklabs.watchtower.enable=true.
Conteneurs actuellement opt-in (13) : vaultwarden, les 5 budgets, teslamate + grafana + mosquitto + node-red, uptime-kuma, healthchecks, backrest.
Volontairement EXCLUS : - Toutes les bases Postgres (vaultwarden_db, teslamate_db, gitea_db, topfrag-db-1) — les upgrades de version majeure cassent la base sans pg_dump/restore manuel - Images locales buildées (gm2-qr:local, pg-dumper:local, caddy-crowdsec:local, topfrag-*:local) — pas de registry d'origine - Watchtower lui-même (par prudence)
Pour activer l'auto-update sur un nouveau service, ajouter le label dans son compose puis redéployer la stack. Pour le désactiver : retirer le label.
Log rotation Docker¶
/etc/docker/daemon.json cape les logs JSON par conteneur :
Important : les options de log sont fixées à la création du conteneur, pas au runtime. Donc toute stack recréée hérite des nouvelles options. Pour purger les logs déjà accumulés sur un conteneur existant sans le recréer :
Gotcha caddy-docker-proxy résolu (mai 2026)¶
Problème historique : avec caddy.reverse_proxy: "{{upstreams PORT}}", Caddy gardait les anciennes IPs en cache après recreation de conteneurs → HTTP 502 jusqu'à ce qu'on redémarre Caddy. C'était récurrent.
Solution appliquée : tous les composes utilisent maintenant caddy.reverse_proxy: <nom_conteneur>:PORT (cf. section Reverse proxy). Caddy résout dynamiquement via le DNS Docker → plus jamais besoin de restart Caddy après une recreation de stack.