Infrastructure Docker auto-hébergée · 22 services · SSO Authentik · CrowdSec · Grafana · PostgreSQL 18
  • Shell 63.6%
  • Python 30.3%
  • HTML 6.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
pepper e2123bd91a fix(renovate): retry rapide après un échec au lieu d'attendre 24h
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>
2026-07-24 14:50:08 +00:00
crowdsec/config Initial commit — stack docker molok.fr 2026-06-27 01:50:51 +00:00
jellyfin Initial commit — stack docker molok.fr 2026-06-27 01:50:51 +00:00
monitoring fix(grafana): ajouter X-Scope-OrgID au datasource Loki 2026-07-07 22:15:44 +00:00
postgres-init Initial commit — stack docker molok.fr 2026-06-27 01:50:51 +00:00
swag fix(swag): corriger cjson.array_mt inexistant dans le bouncer crowdsec 2026-07-16 15:48:21 +00:00
wireguard/config Initial commit — stack docker molok.fr 2026-06-27 01:50:51 +00:00
.gitignore fix(swag): réparer custom-cont-init.d (jamais exécuté) + restreindre whitelist qbittorrent 2026-07-16 00:55:55 +00:00
.yamllint chore(lint): ajouter config yamllint adaptée au style du repo 2026-07-23 15:31:48 +00:00
backup.sh fix(backup): chmod 600 sur l'archive générée 2026-07-15 02:06:52 +00:00
CHANGELOG.md docs: documenter le fix crowdsec metrics.lua dans le CHANGELOG 2026-07-16 15:54:39 +00:00
docker-compose.yml fix(renovate): retry rapide après un échec au lieu d'attendre 24h 2026-07-24 14:50:08 +00:00
git-push.sh chore: synchroniser origin/main après push dans git-push.sh 2026-07-01 16:44:11 +00:00
README.md docs: documenter le durcissement secrets/pids_limit et l'essai pinDigests 2026-07-15 01:08:25 +00:00
renovate.json security(renovate): activer le pin des images par digest 2026-07-24 14:32:28 +00:00
restore.sh fix(restore): normaliser les propriétaires critiques après extraction 2026-07-07 17:15:56 +00:00
RUNBOOK.md docs: documenter le durcissement secrets/pids_limit et l'essai pinDigests 2026-07-15 01:08:25 +00:00

🐳 Stack Docker — molok.fr

Auto-hébergement complet · SSO centralisé · Supervision intégrée · Sauvegardes automatisées

Version Services PostgreSQL Nextcloud Authentik CrowdSec hCaptcha WireGuard Grafana Renovate Licence

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

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 + db mais jamais sur swag → 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 (usernamepreferred_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 auth
  • authentik-location.conf — ajoute auth_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.sh depuis les secrets — aucun mot de passe en clair.
  • Deux Redis séparés pour l'isolation : Authentik (AOF) et Nextcloud (LRU cache).

⚠️ init.sh ne 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 en 600 debian:debian, sauf forgejo_git_token.txt, grafana_admin_password.txt et grafana_oauth_secret.txt en 640 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-init lisent leur secret en 600 grâce à un cap_add: DAC_OVERRIDE explicite (voir docker-compose.yml). Un chmod sur 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)