- Shell 63.6%
- Python 30.3%
- HTML 6.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Confirmé aujourd'hui (2 sessions) : un échec au boot est presque toujours transitoire (DNS/réseau pas encore stabilisé juste après un démarrage ou un down/up complet), pas un vrai problème de token. La boucle attendait 24h avant de retenter quoi qu'il arrive, laissant Renovate inactif toute une journée pour un simple aléa de démarrage. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
| crowdsec/config | ||
| jellyfin | ||
| monitoring | ||
| postgres-init | ||
| swag | ||
| wireguard/config | ||
| .gitignore | ||
| .yamllint | ||
| backup.sh | ||
| CHANGELOG.md | ||
| docker-compose.yml | ||
| git-push.sh | ||
| README.md | ||
| renovate.json | ||
| restore.sh | ||
| RUNBOOK.md | ||
🐳 Stack Docker — molok.fr
Auto-hébergement complet · SSO centralisé · Supervision intégrée · Sauvegardes automatisées
Principe directeur : segmentation et moindre privilège. Chaque service ne voit que ce dont il a besoin — réseaux Docker cloisonnés, secrets en fichiers, capabilities Linux réduites, conteneurs en lecture seule quand c'est possible.
🗂 Sommaire
- 🌐 Vue d'ensemble
- 📦 Services
- 🔗 Réseaux & segmentation
- 🔄 Le chemin d'une requête
- 🔐 Authentification SSO (Authentik)
- 💾 Bases de données & cache
- 📊 Supervision
- 🛡 Sécurité (CrowdSec + hCaptcha)
- 🔄 Mises à jour (Renovate)
- 🔑 Secrets
- 💾 Sauvegarde & restauration
- ⚡ Exploitation au quotidien
- 📁 Arborescence
🌐 Vue d'ensemble
20 services orchestrés par docker-compose.yml. Trois points d'entrée publics seulement :
| Port | Service | Usage |
|---|---|---|
443 / 80 |
🛡 SWAG | Reverse proxy HTTPS — tout le trafic web |
51820/udp |
🔒 WireGuard | VPN d'accès distant |
6881 tcp/udp |
⬇️ qBittorrent | Trafic BitTorrent |
Tout le reste est interne et inaccessible depuis Internet.
Internet
│ :443/:80 :51820/udp :6881
▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌────────────┐
│ SWAG │ reverse proxy │WireGuard │ VPN │qBittorrent │
└───┬────┘ └──────────┘ └────────────┘
│ route par sous-domaine + SSO + CrowdSec
▼
nextcloud · forgejo · jellyfin · grafana · auth (Authentik)
│
▼
PostgreSQL · Redis ×2 (réseaux internes, jamais exposés)
📦 Services
🚪 Front / accès
| Service | Image | Rôle |
|---|---|---|
| swag | lscr.io/linuxserver/swag |
Reverse proxy nginx + Let's Encrypt (DNS challenge). Seul service exposé à Internet et au réseau interne. |
| wireguard | lscr.io/linuxserver/wireguard |
VPN ultra-rapide. Peers générés dans ./wireguard/config. |
🖥 Applications
| Service | URL | Rôle |
|---|---|---|
| ☁️ nextcloud | nextcloud.molok.fr | Cloud de fichiers personnel (équivalent Google Drive). |
| ⏱ nextcloud-cron | — | Tâches planifiées Nextcloud (/cron.sh). |
| 🐙 forgejo | forgejo.molok.fr | Forge Git auto-hébergée (équivalent GitHub). |
| 🎬 jellyfin | jellyfin.molok.fr | Médiathèque (films / séries / musique). |
| 🔧 jellyfin-sso-init | — | Conteneur jetable : configure le plugin SSO de Jellyfin au démarrage via l'API, puis s'arrête. |
| ⬇️ qbittorrent | qbittorrent.molok.fr | Client torrent. Télécharge dans des dossiers que Jellyfin monte en lecture. |
🔐 Identité
| Service | URL | Rôle |
|---|---|---|
| 🔑 authentik-server | auth.molok.fr | Fournisseur d'identité SSO — login, flows, MFA, tokens. |
| ⚙️ authentik-worker | — | Tâches de fond (emails, certificats, sessions). Seul service avec sortie Internet contrôlée. |
🗄 Données
| Service | Rôle |
|---|---|
| 🗃 postgresql | PostgreSQL 18, base partagée (Authentik + Forgejo + Nextcloud). |
| ⚡ redis | Cache & sessions Authentik (persistance AOF). |
| ⚡ redis-nextcloud | Cache Nextcloud — instance isolée. |
📊 Observabilité
| Service | URL | Rôle |
|---|---|---|
| 📈 prometheus | — | Métriques (rétention 30 j / 10 GB). |
| 📋 loki | — | Logs centralisés. |
| 🔭 alloy | — | Agent de collecte : logs conteneurs + métriques cAdvisor. |
| 💻 node-exporter | — | Métriques de la machine hôte. |
| 📊 grafana | grafana.molok.fr | Tableaux de bord — connecté en SSO Authentik. |
🛡 Sécurité
| Service | Rôle |
|---|---|
| 🚨 crowdsec | Moteur IDS/IPS + LAPI. Analyse les logs nginx (SWAG) et les événements Docker (Authentik). Bloque ou soumet un hCaptcha aux IPs malveillantes. |
🔄 Mise à jour
| Service | Rôle |
|---|---|
| 🔍 docker-socket-proxy | Expose l'API Docker en lecture seule pour Alloy. |
| 🤖 renovate | Scanne les images Docker chaque week-end et ouvre des PRs sur Forgejo pour les mises à jour disponibles. |
🔗 Réseaux & segmentation
7 réseaux Docker. Les réseaux internal n'ont aucun accès Internet — un service n'est joignable que par ceux qui partagent un de ses réseaux.
| Réseau | Internet | Membres | Rôle |
|---|---|---|---|
swag |
✅ | swag + apps web | Trafic public entrant |
authentik |
🔒 | authentik, postgres, redis | SSO isolé |
db |
🔒 | postgres, redis-nextcloud, apps | Accès bases cloisonné |
monitoring |
🔒 | pile d'observabilité | Métriques & logs |
socket-proxy |
🔒 | alloy + crowdsec + docker-socket-proxy | API Docker lecture seule |
outbound |
✅ | authentik-worker, renovate | Sortie Internet contrôlée |
crowdsec |
✅ | swag + crowdsec | Bouncer↔LAPI + hub (MAJ collections) |
💡 Exemple : PostgreSQL est sur
authentik+dbmais jamais surswag→ il est structurellement inatteignable depuis Internet.
🔄 Le chemin d'une requête
1. 🌐 Navigateur → https://<app>.molok.fr
2. 🛡 SWAG termine le TLS, route selon le sous-domaine
3. 🚨 Bouncer CrowdSec interroge la LAPI :
IP bannie → 403
IP suspecte → hCaptcha
IP inconnue → passe
4. 🔐 Authentification :
OIDC natif → l'app gère le dialogue OAuth2
Forward-auth → SWAG interroge Authentik
5. 📦 La requête atteint le conteneur applicatif
🔐 Authentification SSO (Authentik)
Tout le monde se connecte via Authentik (auth.molok.fr). Deux mécanismes coexistent :
| App | Mécanisme |
|---|---|
| Grafana, Forgejo, Jellyfin | 🔑 OIDC natif (OAuth2 Authorization Code) |
| qBittorrent | 🛡 Forward-auth (SWAG fait le gardien) |
| Nextcloud | 🔒 Login natif (pas de SSO câblé) |
📖 Pattern A — OIDC natif (Grafana, Forgejo, Jellyfin)
Flux OAuth2 « Authorization Code ». L'app gère elle-même le dialogue OIDC.
(Grafana a AUTO_LOGIN=true → redirige direct, sans afficher son formulaire.)
1. Navigateur → app : GET <app>.molok.fr
2. app → navigateur : 302 vers auth.molok.fr/application/o/authorize/
(client_id, redirect_uri, scope=openid email profile, state)
3. navigateur → Authentik (via SWAG) : page de login si pas de session (Redis)
4. login validé contre PostgreSQL → session créée dans Redis
5. Authentik → navigateur : 302 vers app/...?code=XXX
6. [BACK-CHANNEL serveur↔serveur, sans le navigateur]
app → Authentik /token (code + client_secret) → access_token + id_token
app → Authentik /userinfo → { email, name, groups[] }
7. app crée sa propre session (cookie) → connecté
Endpoints Authentik :
- authorize :
https://auth.molok.fr/application/o/authorize/ - token :
https://auth.molok.fr/application/o/token/ - userinfo :
https://auth.molok.fr/application/o/userinfo/
Mapping de rôle Grafana :
contains(groups[*], 'grafana-admins') && 'GrafanaAdmin' || 'Viewer'
Forgejo : ENABLE_AUTO_REGISTRATION=true → compte créé automatiquement à la première connexion SSO (username ← preferred_username).
📖 Pattern B — Forward-auth (qBittorrent)
qBittorrent n'a pas de SSO natif : SWAG (nginx) fait le gardien via l'outpost Authentik.
1. Navigateur → SWAG : GET qbittorrent.molok.fr
2. SWAG → Authentik (sous-requête) : /outpost.goauthentik.io/auth/nginx
3a. 200 → SWAG proxifie vers qBittorrent (X-authentik-email/-groups/…)
3b. 401 → SWAG renvoie 302 vers .../start?rd=<url> → login → retour
Snippets nginx (./swag/conf/nginx/) :
authentik-server.conf— expose/outpost.goauthentik.io/...sans authauthentik-location.conf— ajouteauth_request+error_page 401
📖 Déconnexion (single logout)
Grafana : GF_AUTH_SIGNOUT_REDIRECT_URL=.../end-session/
→ se déconnecter de Grafana ferme aussi la session Authentik (invalidée dans Redis).
💾 Bases de données & cache
- 🗃 PostgreSQL 18 partagé entre Authentik, Forgejo et Nextcloud. Données dans
./postgres/data(sous-dossier versionné18/docker). - Les bases sont créées à la première init par
postgres-init/init.shdepuis les secrets — aucun mot de passe en clair. - ⚡ Deux Redis séparés pour l'isolation : Authentik (AOF) et Nextcloud (LRU cache).
⚠️
init.shne s'exécute qu'une seule fois (data dir vide). Pour changer un mot de passe existant :ALTER ROLE … PASSWORD+ mise à jour du fichier secret.
📊 Supervision
💻 node-exporter ──(métriques hôte)──┐
🔭 alloy ──(métriques conteneurs)───→ 📈 Prometheus ──┐
🔭 alloy ──(logs conteneurs)────────→ 📋 Loki ────────┤→ 📊 Grafana
Alloy découvre les conteneurs sans accès direct au socket Docker : il passe par docker-socket-proxy (lecture seule stricte, POST=0).
🛡 Sécurité (CrowdSec + hCaptcha)
🌐 Internet → 🛡 SWAG (bouncer lua) → 🚨 CrowdSec LAPI
│
logs nginx ──────┤
events Docker ───┘ (authentik-server)
CrowdSec détecte les comportements suspects (brute-force, CVE HTTP, scans) en temps réel.
| Remédiation | Comportement |
|---|---|
🚫 ban |
IP bloquée — 403 immédiat |
🤖 captcha |
Page hCaptcha imposée avant accès |
🤖 hCaptcha
Provider : hCaptcha (respect de la vie privée, sans Google).
Les clés sont injectées via Docker secrets dans SWAG :
CROWDSEC_CAPTCHA_PROVIDER=hcaptcha
FILE__CROWDSEC_SITE_KEY=/run/secrets/hcaptcha_site_key
FILE__CROWDSEC_SECRET_KEY=/run/secrets/hcaptcha_secret_key
Pour renouveler les clés : mettre à jour les fichiers secrets puis docker compose up -d --force-recreate swag.
🔧 Commandes CrowdSec utiles
# IPs bannies
docker exec crowdsec cscli decisions list
# Débannir une IP
docker exec crowdsec cscli decisions delete --ip <ip>
# Alertes sur une IP
docker exec crowdsec cscli alerts list --ip <ip>
# État du bouncer
docker exec crowdsec cscli bouncers list
🔄 Mises à jour (Renovate)
Les images sont mises à jour via Renovate — pas de mise à jour automatique silencieuse, chaque changement passe par une PR.
Chaque week-end :
🤖 Renovate scanne les versions disponibles
→ ouvre une PR sur Forgejo par groupe d'images
→ tu reviews & merges manuellement
→ git pull && sudo docker compose pull && sudo docker compose up -d
Les images sont regroupées en trois catégories dans renovate.json :
| Groupe | Images | Merge |
|---|---|---|
| 🔴 stack critique | postgres, authentik, nextcloud, forgejo, redis | Manuel obligatoire |
| 🟡 monitoring | grafana, loki, alloy, prometheus, node-exporter | Manuel |
| 🟢 infra | swag, wireguard, qbittorrent, crowdsec, jellyfin, docker-socket-proxy | Manuel |
Les versions sont épinglées dans
docker-compose.yml— Renovate ouvre une PR pour chaque bump, ce qui permet de tester avant d'appliquer.
🔑 Secrets
Aucun mot de passe en clair dans l'environnement. Les secrets sont des fichiers (./secrets/*.txt) montés en /run/secrets/....
Les services ne supportant pas les variables *_FILE (Redis, Authentik, Grafana) ont un entrypoint qui fait export VAR="$(cat /run/secrets/…)" avant de lancer le programme.
⚠️ Permissions : tous les fichiers de
./secrets/sont en600 debian:debian, saufforgejo_git_token.txt,grafana_admin_password.txtetgrafana_oauth_secret.txten640 debian:root(les images renovate/grafana tournent en UID non-root mais GID 0/root, elles lisent via le bit groupe).redis/redis-nextcloud/jellyfin-sso-initlisent leur secret en600grâce à uncap_add: DAC_OVERRIDEexplicite (voirdocker-compose.yml). Unchmodsur un secret ne casse ou ne répare rien tant que le conteneur concerné n'est pas recréé (docker compose up -d --force-recreate <service>) — un conteneur déjà démarré garde sa lecture précédente en mémoire.
📋 Liste complète des secrets attendus dans ./secrets/
authentik_db_password.txt authentik_secret_key.txt
forgejo_db_password.txt nextcloud_db_password.txt
nextcloud_admin_password.txt redis_password.txt
redis_nextcloud_password.txt grafana_oauth_secret.txt
grafana_admin_password.txt jellyfin_api_key.txt
jellyfin_sso_secret.txt crowdsec_api_key.txt
hcaptcha_site_key.txt hcaptcha_secret_key.txt
forgejo_git_token.txt
🔒
secrets/,.env*, les dumps et les archives sont exclus par.gitignore.
💾 Sauvegarde & restauration
backup.sh
sudo ./backup.sh # ❄️ à froid (recommandé) : down -v → tar → up -d
sudo ./backup.sh --hot # 🔥 à chaud : sans arrêter la stack (moins sûr pour les bases)
| Fonctionnalité | Détail |
|---|---|
| ❄️ Mode froid | Arrêt stack → tar cohérent → redémarrage |
| ✅ Intégrité | Archive vérifiée après création |
| 🔁 Rotation | Conserve les 7 plus récentes |
| 🧹 Nettoyage | Supprime les images Docker dangling |
| 📡 Copie hors-site | Vers REMOTE_DIR si monté (sshfs) |
Données exclues (régénérables) : WAL Prometheus/Loki/Alloy, cache Jellyfin, téléchargements qBittorrent.
restore.sh
Opération inverse — voir le script pour les détails.
⚡ Exploitation au quotidien
# 📋 État de la stack
sudo docker compose ps
# 📜 Logs d'un service
sudo docker compose logs -f <service>
# 🔄 Recréer un service après modif du compose
sudo docker compose up -d <service>
# ✅ Valider le compose
sudo docker compose config --quiet && echo OK
# 📦 Appliquer une PR Renovate (après merge)
git pull && sudo docker compose pull && sudo docker compose up -d
🗃 Versionnement (git)
Le dépôt est hébergé sur forgejo.molok.fr/pepper/docker-infra (privé).
Le serveur ne pouvant pas se joindre lui-même via son IP externe (hairpin NAT), le push passe par le réseau Docker interne via un conteneur éphémère :
./git-push.sh
🔑 Le token d'accès Forgejo est stocké dans
secrets/forgejo_git_token.txt.
📁 Arborescence
docker-compose.yml # définition de toute la stack
renovate.json # 🤖 règles de mise à jour automatique des images
.env # variables (domaine, PUID/PGID, SWAG, WireGuard…)
secrets/ # 🔑 mots de passe et clés (un fichier par secret)
postgres-init/init.sh # création des bases applicatives à la 1ʳᵉ init
backup.sh / restore.sh # 💾 sauvegarde / restauration
git-push.sh # 🗃 push vers Forgejo via le réseau Docker interne
postgres/ authentik/ forgejo/ nextcloud/ qbittorrent/
swag/ wireguard/ jellyfin/ monitoring/ crowdsec/
monitoring/
├── prometheus/ (prometheus.yml + data)
├── loki/ (config.yml + data)
├── alloy/ (config.alloy + data)
└── grafana/ (provisioning + data)