Aller au contenu

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.yml sur l'hôte.
  • Mode : caddy-docker-proxy lit les labels Docker des conteneurs sur proxy_net pour 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 :

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "5" }
}
→ chaque conteneur écrit au max 50 Mo de logs (10 Mo × 5 fichiers rotatés), prévient ce qui s'était passé sur Kluedo (2,6 Go de json-log accumulés).

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 :

sudo truncate -s 0 /var/lib/docker/containers/<id>/<id>-json.log

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.