La stack, en détail

Tout est infrastructure-as-code dans un seul dépôt : cluster/ pour les briques transverses, apps/ pour les applications. Un git clone + un make reconstruisent l'ensemble.

Le chemin d'une requête

  Visiteur (Internet)
       │  HTTPS
       ▼
  ┌─────────────────┐   TLS terminé chez Cloudflare
  │   Cloudflare     │   (aucun port ouvert sur la box)
  └────────┬────────┘
       │  tunnel chiffré
       ▼
  ┌─────────────────┐   cloudflared → traefik:80
  │  Docker Swarm    │
  │                  │   1. error-pages + crowdsec  (4xx/5xx soignés, IPS + WAF)
  │  ┌────────────┐  │   2. forwarded-https         (schéma réel = https)
  │  │  Traefik   │──┼─▶ 3. authelia                (SSO/2FA si app privée)
  │  └─────┬──────┘  │   4. sablier                 (réveille l'app si endormie)
  │        ▼         │
  │  ┌────────────┐  │   nginx · php-fpm · postgres
  │  │   App      │  │   …ou nginx-s3-gateway ← bucket R2 (cette vitrine)
  │  └────────────┘  │
  └─────────────────┘
       │
       ▼  restic chiffré (planifié)
   Cloudflare R2  (backups hors-site)

Les choix qui comptent

Zéro port ouvert

Aucune redirection de port sur la box : tout le trafic entrant passe par un tunnel sortant Cloudflare. La surface d'attaque réseau côté maison est nulle.

Scale-to-zero

Les apps peu sollicitées descendent à 0 réplica et se réveillent à la première requête autorisée. RAM économisée sur du matériel modeste, avec une page d'attente soignée.

SSO d'abord, réveil ensuite

Authelia s'exécute avant Sablier : une requête non authentifiée est renvoyée au portail sans même réveiller l'application. On ne dépense des ressources que pour de vrais utilisateurs.

Reconstruisible

Tout est versionné et déployable en ssh + docker stack deploy, sans dépendre d'une UI. Un plan de reprise existe pour repartir d'un dépôt git et des sauvegardes.

Déploiement continu, secrets cloisonnés

Cette vitrine se reconstruit sur git push : GitHub Actions build, puis n8n déploie vers R2 et vide le cache — sans coupure (rolling). Les clés d'écriture R2 restent côté homelab (Docker secrets), jamais sur GitHub.

On ne s'alerte pas de sa propre panne

Tunnel, bases, services : tout part en notification push si ça défaille. Mais pas d'alerte « le canal de notif est mort » — elle transiterait par ce même canal. Un système ne surveille pas sa propre alerte ; c'est le rôle d'un veilleur externe. On préfère connaître ses angles morts plutôt que s'offrir une fausse assurance.

Le résolveur ne vit pas dans le cluster qu'il sert

Le DNS du LAN (AdGuard) est géré par le cluster — config versionnée, métriques, alertes, accès SSO — mais il tourne dehors, sur sa propre machine. Le mettre dans le Swarm créerait une dépendance circulaire : un déplacement de nœud couperait le DNS de toute la maison, et donc les outils pour le réparer. On intègre la gestion, pas le moteur.

Pas de point unique de défaillance

Les 3 Pi sont managers (quorum raft 2/3), l'ingress tourne en actif/actif, les bases sont en cluster Patroni (failover automatique) et chaque app flotte entre les nœuds — jusqu'à la couche d'authentification (Authelia + sessions) répliquée. Perdre un nœud — à chaud ou par crash — ne coupe ni le site, ni le pilotage, ni les données : validé par une matrice de 6 drills (sortie propre + crash sur chacun des 3 nœuds). Les Pi restent nomades, déplaçables un par un.

La source de vérité des secrets ne vit pas dans le cluster

Bitwarden Secrets Manager est le master : chaque stack reçoit ses secrets sous forme de fichier rendu, le contrat (les noms, jamais les valeurs) est versionné en git. Le runtime ne dépend jamais de Bitwarden — seuls la rotation et le disaster recovery y touchent. Perdre le disque du manager, c'est un make secrets-render ; faire tourner une clé, trois commandes.

Le matériel

Le détail des briques

Orchestration

Docker Swarm3 nœuds Pi (NVMe), tous managers — quorum raft 2/3, tolère la perte d'un nœud
PortainerUI + API (pilotée par le déploiement)

Ingress / réseau

Traefikv3 — actif/actif sur les 3 managers ; reverse-proxy, métriques + traces OTLP
Cloudflare Tunnelexposition publique, 0 port ouvert — cloudflared en HA
Sablierscale-to-zero + page d'attente
error-pagespages 4xx/5xx soignées, globales
AdGuard HomeDNS du LAN (hors-cluster, par principe) — filtrage pub/malware, DoH chiffré ; piloté en config-as-code, observé, en SSO

Sécurité / identité

CrowdSecIPS + WAF (CRS) — réputation d'IP + AppSec
AutheliaSSO + 2FA (forwardAuth & OIDC) — entièrement HA : 2 répliques + sessions Valkey Sentinel
Vaultwardengestionnaire de mots de passe (Postgres HA, clé en secret)

Applications

Stickers Managerl'app métier (nginx + php-fpm)
nginx-s3-gatewaysert cette vitrine depuis un bucket R2
n8nautomatisation — pilote le déploiement continu

Données / sauvegarde

PostgreSQL + Patroniune base dédiée par app, chacune en cluster 2 nœuds — failover automatique (etcd + HAProxy)
restic → Cloudflare R2backups chiffrés hors-site, 2 agents
Backrestorchestration des sauvegardes

Secrets

Bitwarden Secrets Managerla source de vérité (~70 secrets) — plus aucun mot de passe en clair dans les .env
bws + secrets-sync.shrendu par stack en .env.secrets (0600) ; rotation = 3 commandes ; comparaisons md5, jamais une valeur affichée
Docker secretsles credentials runtime (clés RSA, tokens de jobs) — mirrorés dans Bitwarden pour la reprise

Observabilité

Prometheusmétriques cluster, apps & bases — un exporter par cible
Loki + Alloyagrégation des logs (global shipper)
Tempo + OTel Collectortraces distribuées (service map)
Grafanadashboards par service (n8n, Postgres, tunnel…) + suivi des releases + alertes + pont métriques↔logs↔traces
ntfynotifications push — alertes Grafana, déploiements & nouvelles releases

CI/CD & IaC

GitHub Actionsbuild du site → artefact (jamais de clé R2)
n8n + Docker secretsdéploiement vers R2 + flush, secrets côté homelab
Makefile + scriptsssh + docker stack deploy ; secrets rendus depuis Bitwarden (make secrets-render)
Gittout versionné — github.com/zebby76/zebbox.net

Le code de cette infrastructure est public : github.com/zebby76/zebbox.net