Compare commits
24 Commits
9a2722def3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| aab8280ff8 | |||
| dbf771515f | |||
| dea4604828 | |||
| 0e301e76bd | |||
| 3f6a3475c8 | |||
| 3844100012 | |||
| 4ad4bb59c6 | |||
| 948358971d | |||
| 1b221dfec8 | |||
| 885c3b62ea | |||
| c7147957b1 | |||
| e6a8ad6547 | |||
| 1c05fd3578 | |||
| 0584856ea3 | |||
| b002113473 | |||
| b602d42c92 | |||
| 8fdf461ae1 | |||
| 306667b6e1 | |||
| c2cf727bdf | |||
| 3c70a467ed | |||
| b220593def | |||
| 322f2f89b5 | |||
| 122881e539 | |||
| 10fff06464 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -64,3 +64,7 @@ terraform.rc
|
||||
*.agekey
|
||||
age-key.txt
|
||||
keys.txt
|
||||
|
||||
# Le stockage ACME de Traefik : clé de compte Let's Encrypt et clés privées de
|
||||
# tous les certificats. Vit sur le serveur uniquement, jamais dans le dépôt.
|
||||
acme.json
|
||||
|
||||
@@ -285,3 +285,19 @@ resource "ovh_domain_zone_record" "maintenant_aaaa" {
|
||||
ttl = 3600
|
||||
target = "2001:41d0:2:bbb6:0:0:0:1"
|
||||
}
|
||||
|
||||
resource "ovh_domain_zone_record" "forge_a" {
|
||||
zone = "opytex.org"
|
||||
subdomain = "forge"
|
||||
fieldtype = "A"
|
||||
ttl = 3600
|
||||
target = "188.165.240.182"
|
||||
}
|
||||
|
||||
resource "ovh_domain_zone_record" "forge_aaaa" {
|
||||
zone = "opytex.org"
|
||||
subdomain = "forge"
|
||||
fieldtype = "AAAA"
|
||||
ttl = 3600
|
||||
target = "2001:41d0:2:bbb6:0:0:0:1"
|
||||
}
|
||||
|
||||
189
stacks/forge.opytex.org/README.md
Normal file
189
stacks/forge.opytex.org/README.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Forgejo — forge.opytex.org
|
||||
|
||||
Forge git. Quatre conteneurs : `forgejo` (l'application), `postgres`,
|
||||
`cache` (Valkey) et `runner` (Forgejo Actions).
|
||||
|
||||
Le nom de ce dossier donne le nom de projet Compose `forgeopytexorg`, dont
|
||||
dérivent `forgeopytexorg_forgejo_data` et le nom du dump de base. **Le renommer
|
||||
ferait repartir la forge sur des volumes vides.**
|
||||
|
||||
## Déploiement
|
||||
|
||||
Depuis ce dossier, sur chioggia :
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml 'docker compose up -d'
|
||||
```
|
||||
|
||||
Les secrets sont déclarés `${VAR:?}` : **toute** commande `docker compose`
|
||||
échoue sans l'enveloppe `sops exec-env`, y compris `logs`, `ps` et `exec`.
|
||||
|
||||
Après une rotation de secret, `up -d` répond « up to date » et garde l'ancienne
|
||||
valeur — il faut `--force-recreate`.
|
||||
|
||||
## Mise en service, dans l'ordre
|
||||
|
||||
Les cinq étapes ci-dessous sont à faire une seule fois, **dans cet ordre**.
|
||||
L'ordre n'est pas indifférent : voir l'étape 3.
|
||||
|
||||
### 1. Remplir les secrets
|
||||
|
||||
Deux valeurs, toutes deux en **placeholder visible** (`REMPLACER-…`) :
|
||||
|
||||
```bash
|
||||
sops edit secrets.env.yaml
|
||||
```
|
||||
|
||||
| Clé | Origine |
|
||||
|---|---|
|
||||
| `POSTGRES_PASSWORD` | à générer (`pwgen -s 32 1`) |
|
||||
| `SSO_CLIENT_SECRET` | copié depuis le provider Authentik créé à l'étape 2 |
|
||||
|
||||
`SSO_CLIENT_SECRET` n'est **pas** lu par le conteneur : il est conservé ici pour
|
||||
ne pas être perdu, et injecté à la main dans la commande `add-oauth` ci-dessous.
|
||||
|
||||
### 2. Créer le provider OIDC dans Authentik
|
||||
|
||||
Application + provider OAuth2/OpenID, avec pour URL de redirection :
|
||||
|
||||
```
|
||||
https://forge.opytex.org/user/oauth2/authentik/callback
|
||||
```
|
||||
|
||||
Le segment `authentik` est le nom donné à la source à l'étape 4 : les deux
|
||||
doivent coïncider. Le reste de l'URL suit `FORGEJO__server__ROOT_URL` — si le
|
||||
domaine de la forge change, cette redirection doit changer avec lui.
|
||||
|
||||
### 3. Créer le compte administrateur local
|
||||
|
||||
**Avant toute connexion SSO.** Forgejo donne les droits d'administration au
|
||||
premier utilisateur créé : si quelqu'un se connecte via Authentik avant cette
|
||||
étape, c'est son compte personnel qui devient administrateur de l'instance.
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml 'docker compose exec -u git forgejo \
|
||||
forgejo admin user create --admin \
|
||||
--username waha-local \
|
||||
--email waha-local@opytex.org \
|
||||
--random-password'
|
||||
```
|
||||
|
||||
Le mot de passe s'affiche une seule fois : le ranger dans `pass` immédiatement.
|
||||
|
||||
Ce compte est **volontairement local**, hors SSO. C'est la même porte de secours
|
||||
que sur `paperless.opytex.org` : il reste utilisable si Authentik tombe ou si la
|
||||
configuration OIDC est cassée — y compris pour réparer cette configuration.
|
||||
|
||||
Il est aussi volontairement **distinct de tout compte personnel** : les comptes
|
||||
nominatifs créés par SSO restent des utilisateurs ordinaires. Après la première
|
||||
connexion SSO, vérifier dans *Administration → Utilisateurs* qu'aucun compte
|
||||
nominatif ne porte le drapeau administrateur, et le retirer le cas échéant.
|
||||
|
||||
### 4. Déclarer la source d'authentification
|
||||
|
||||
Une seule fois, forge démarrée :
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml 'docker compose exec -u git forgejo \
|
||||
forgejo admin auth add-oauth \
|
||||
--name authentik \
|
||||
--provider openidConnect \
|
||||
--key <CLIENT_ID_AUTHENTIK> \
|
||||
--secret "$SSO_CLIENT_SECRET" \
|
||||
--auto-discover-url https://sso.opytex.org/application/o/forgejo/.well-known/openid-configuration \
|
||||
--scopes "openid email profile"'
|
||||
```
|
||||
|
||||
Le `client_id` n'est pas un secret : il peut rester en clair dans cette commande.
|
||||
|
||||
**Vérifier immédiatement qu'un compte se crée bien à la première connexion.**
|
||||
Le trio `DISABLE_REGISTRATION` / `ALLOW_ONLY_EXTERNAL_REGISTRATION` /
|
||||
`ENABLE_AUTO_REGISTRATION` d'`env_file` est contre-intuitif : mettre
|
||||
`DISABLE_REGISTRATION=true`, ce qui semble être le réglage prudent, bloque
|
||||
aussi l'auto-création via Authentik et **verrouille la forge pour tout le
|
||||
monde**. C'est là que le compte de l'étape 3 sert.
|
||||
|
||||
L'auto-création vaut pour **toute personne autorisée par Authentik** à atteindre
|
||||
l'application. Restreindre cet accès est un réglage d'Authentik, pas de Forgejo :
|
||||
lier un groupe à l'application, sans quoi n'importe quel compte de l'annuaire se
|
||||
verra ouvrir un compte sur la forge à sa première visite.
|
||||
|
||||
### 5. Raccorder le runner
|
||||
|
||||
Il n'y a **pas d'étape d'enregistrement** : la sous-commande `register` du
|
||||
runner est dépréciée. C'est la forge qui crée le runner et délivre un couple
|
||||
**UUID + jeton**, que le démon présente ensuite comme un identifiant et un mot
|
||||
de passe.
|
||||
|
||||
Dans *Administration → Actions → Runners*, créer un runner. La page affiche
|
||||
l'UUID et le jeton — **le jeton n'est montré qu'une fois.**
|
||||
|
||||
**a.** Reporter l'UUID dans `runner/config.yaml`, à la place de
|
||||
`REMPLACER-UUID-DU-RUNNER`. Ce n'est pas un secret, il reste en clair dans le
|
||||
dépôt.
|
||||
|
||||
**b.** Déposer le jeton dans le volume. Il ne doit toucher ni le dépôt, ni le
|
||||
disque de l'hôte, ni l'historique du shell — d'où la lecture sur l'entrée
|
||||
standard :
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml 'docker compose run --rm --entrypoint sh runner \
|
||||
-c "umask 077; cat > /data/runner-token"'
|
||||
# coller le jeton, puis Ctrl-D
|
||||
```
|
||||
|
||||
**c.** Démarrer : `sops exec-env secrets.env.yaml 'docker compose up -d runner'`.
|
||||
|
||||
Le runner doit apparaître « en ligne » dans l'administration.
|
||||
|
||||
Le jeton vit dans `forgejo_runner_data`, donc `docker compose up -d
|
||||
--force-recreate` et les redémarrages le préservent. Seul un `down -v` — qui
|
||||
détruirait aussi tous les dépôts — le ferait disparaître. Dans ce cas :
|
||||
supprimer le runner dans l'administration, en créer un neuf, et refaire **a**
|
||||
et **b** ; l'UUID change à chaque fois.
|
||||
|
||||
## Dépendance MinIO
|
||||
|
||||
`runner/config.yaml` déclare le label `minio-mc`. Deux choses en dépendent, à
|
||||
reprendre le jour du passage de `storage.opytex.org` (MinIO) à Garage :
|
||||
|
||||
1. **Le label** `minio-mc:docker://minio/mc:latest` — image `mc` fournie aux
|
||||
jobs qui le demandent en `runs-on`.
|
||||
2. **L'action `lafrite/minio-deploy-action`**, appelée en URL absolue dans les
|
||||
workflows, qui consomme les secrets Actions `MINIO_ENDPOINT`,
|
||||
`MINIO_ACCESS_KEY` et `MINIO_SECRET_KEY`.
|
||||
|
||||
Le tag `latest` combiné à `force_pull: true` fait suivre `mc` en amont sans
|
||||
préavis. Rien n'est changé ici, c'est un constat.
|
||||
|
||||
## Volumes et sauvegarde
|
||||
|
||||
| Volume | Contenu | Sauvegarde |
|
||||
|---|---|---|
|
||||
| `forgeopytexorg_forgejo_data` | **tous les dépôts**, config, avatars, paquets | label `backup.enable=true` |
|
||||
| `forgeopytexorg_forgejo_db` | base PostgreSQL | dump SQL (`backup.type=postgres` sur le conteneur) |
|
||||
| `forgeopytexorg_forgejo_runner_data` | le jeton du runner (`runner-token`) | non sauvegardé — se recrée depuis l'administration |
|
||||
|
||||
Les labels de volume ne s'appliquent qu'à la création : ceux-ci sont donc
|
||||
effectifs. Ne pas en retirer ni en ajouter par la suite — Compose détecte la
|
||||
divergence entre le volume et sa déclaration, et propose de le **recréer**.
|
||||
|
||||
`forgejo_db` n'a délibérément aucun label : sauvegarder les fichiers d'un
|
||||
PostgreSQL en cours d'écriture ne donne pas une base restaurable.
|
||||
|
||||
Ajouter `forgeopytexorg_forgejo_data` à `MANUAL_VOLUMES` (`backup.conf`) reste
|
||||
utile en ceinture-bretelles, comme pour les autres stacks — restic déduplique.
|
||||
|
||||
## Version
|
||||
|
||||
Forgejo **v15.0.3 (LTS)**, supportée jusqu'en juillet 2027. La branche non-LTS
|
||||
(v16) a un horizon de support de quelques mois seulement : inadapté pour la
|
||||
forge qui héberge le dépôt `infra`.
|
||||
|
||||
Ne pas monter de version majeure sans sauvegarde vérifiée — les migrations de
|
||||
schéma ne se reviennent pas.
|
||||
|
||||
Références :
|
||||
|
||||
- https://forgejo.org/docs/latest/admin/
|
||||
- https://forgejo.org/releases/
|
||||
99
stacks/forge.opytex.org/docker-compose.yml
Normal file
99
stacks/forge.opytex.org/docker-compose.yml
Normal file
@@ -0,0 +1,99 @@
|
||||
services:
|
||||
forgejo:
|
||||
image: codeberg.org/forgejo/forgejo:15.0.3
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- postgres
|
||||
- cache
|
||||
env_file:
|
||||
- env_file
|
||||
environment:
|
||||
FORGEJO__database__PASSWD: ${POSTGRES_PASSWORD:?}
|
||||
volumes:
|
||||
- forgejo_data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
# Port hôte du SSH git. Doit rester cohérent avec
|
||||
# FORGEJO__server__SSH_PORT dans env_file, qui est ce que la forge
|
||||
# annonce dans ses URL de clone.
|
||||
- "2223:22"
|
||||
networks:
|
||||
- default
|
||||
- traefik-proxy
|
||||
- smtp
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-proxy
|
||||
- traefik.http.routers.forgejo.entrypoints=web-secure
|
||||
- traefik.http.routers.forgejo.rule=Host(`forge.opytex.org`)
|
||||
- traefik.http.routers.forgejo.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.forgejo.middlewares=crowdsec@file
|
||||
- traefik.http.services.forgejo.loadbalancer.server.port=3000
|
||||
|
||||
postgres:
|
||||
image: postgres:18
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- env_file
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
|
||||
volumes:
|
||||
- forgejo_db:/var/lib/postgresql
|
||||
networks:
|
||||
- default
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
- backup.enable=true
|
||||
- backup.type=postgres
|
||||
|
||||
cache:
|
||||
# Pas de `container_name` : le nom serait global au démon. Ce Valkey est
|
||||
# réservé à cette forge — deux forges partageant un même Redis se volent
|
||||
# leurs événements, avec des symptômes qui n'y font jamais penser.
|
||||
image: valkey/valkey:9.0.3-alpine
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "valkey-cli", "ping"]
|
||||
interval: 15s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "1m"
|
||||
networks:
|
||||
- default
|
||||
|
||||
|
||||
runner:
|
||||
image: data.forgejo.org/forgejo/runner:13
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- forgejo
|
||||
# URL, UUID et emplacement du jeton sont dans runner/config.yaml.
|
||||
command: ["forgejo-runner", "daemon", "--config", "/config.yaml"]
|
||||
volumes:
|
||||
- ./runner/config.yaml:/config.yaml:ro
|
||||
- forgejo_runner_data:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
group_add:
|
||||
- "1002"
|
||||
networks:
|
||||
- default
|
||||
|
||||
volumes:
|
||||
# Les labels de volume ne s'appliquent qu'à la création : ceux-ci sont
|
||||
# effectifs. Ne pas en ajouter ni en retirer ensuite, Compose proposerait de
|
||||
# recréer le volume.
|
||||
forgejo_data:
|
||||
labels:
|
||||
backup.enable: true
|
||||
forgejo_db:
|
||||
forgejo_runner_data:
|
||||
|
||||
networks:
|
||||
traefik-proxy:
|
||||
external: true
|
||||
smtp:
|
||||
external: true
|
||||
80
stacks/forge.opytex.org/env_file
Normal file
80
stacks/forge.opytex.org/env_file
Normal file
@@ -0,0 +1,80 @@
|
||||
# =============================================================================
|
||||
# FORGEJO — configuration NON sensible
|
||||
# =============================================================================
|
||||
# Seul POSTGRES_PASSWORD est chiffré (secrets.env.yaml, injecté par
|
||||
# `sops exec-env`). Le secret client Authentik n'est pas ici : il n'est pas lu
|
||||
# par le conteneur, il se saisit une fois via `forgejo admin auth add-oauth`.
|
||||
#
|
||||
# Ce fichier ne contient QUE ce que les conteneurs consomment. La version
|
||||
# d'image et les règles Traefik sont en dur dans docker-compose.yml :
|
||||
# `env_file:` n'alimente pas l'interpolation ${...}, et une version en dur est
|
||||
# détectée par Renovate.
|
||||
# =============================================================================
|
||||
|
||||
# --- Base de données ---
|
||||
# POSTGRES_* est lu par le conteneur postgres ET par le script de sauvegarde,
|
||||
# qui fait `docker exec printenv POSTGRES_DB / POSTGRES_USER`. FORGEJO__database__*
|
||||
# est lu par l'application. Les deux jeux doivent porter la MÊME valeur.
|
||||
POSTGRES_USER=forgejo
|
||||
POSTGRES_DB=forgejo
|
||||
|
||||
FORGEJO__database__DB_TYPE=postgres
|
||||
FORGEJO__database__HOST=postgres:5432
|
||||
FORGEJO__database__NAME=forgejo
|
||||
FORGEJO__database__USER=forgejo
|
||||
|
||||
# --- Conteneur ---
|
||||
USER_UID=1000
|
||||
USER_GID=1000
|
||||
TZ=Europe/Paris
|
||||
|
||||
# --- URL publique ---
|
||||
# ROOT_URL est recopié dans les URL de clone affichées, dans les webhooks et
|
||||
# dans le redirect OIDC : le changer suppose de reprendre le provider Authentik
|
||||
# et de recréer les webhooks.
|
||||
FORGEJO__server__DOMAIN=forge.opytex.org
|
||||
FORGEJO__server__ROOT_URL=https://forge.opytex.org/
|
||||
FORGEJO__server__SSH_DOMAIN=forge.opytex.org
|
||||
FORGEJO__server__SSH_PORT=2223
|
||||
FORGEJO__server__SSH_LISTEN_PORT=22
|
||||
|
||||
# --- Cache ---
|
||||
# `cache` est le service Valkey de CETTE stack, et doit lui rester réservé :
|
||||
# deux forges partageant un même Redis se volent leurs événements, avec des
|
||||
# symptômes trompeurs (`AesDecrypt invalid decrypted base64 string`).
|
||||
FORGEJO__cache__ENABLED=true
|
||||
FORGEJO__cache__ADAPTER=redis
|
||||
FORGEJO__cache__HOST=redis://cache:6379/0?pool_size=100&idle_timeout=180s
|
||||
FORGEJO__cache__ITEM_TTL=24h
|
||||
|
||||
# --- Inscriptions & SSO (Authentik) ---
|
||||
# Combinaison volontaire et contre-intuitive : DISABLE_REGISTRATION=true
|
||||
# bloquerait AUSSI l'auto-création via OIDC, et plus personne ne pourrait
|
||||
# entrer. C'est ALLOW_ONLY_EXTERNAL_REGISTRATION qui interdit l'inscription
|
||||
# locale tout en laissant passer Authentik.
|
||||
FORGEJO__service__DISABLE_REGISTRATION=false
|
||||
FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION=true
|
||||
FORGEJO__service__SHOW_REGISTRATION_BUTTON=false
|
||||
FORGEJO__openid__ENABLE_OPENID_SIGNIN=false
|
||||
FORGEJO__oauth2_client__ENABLE_AUTO_REGISTRATION=true
|
||||
FORGEJO__oauth2_client__ACCOUNT_LINKING=auto
|
||||
FORGEJO__oauth2_client__USERNAME=nickname
|
||||
FORGEJO__oauth2_client__UPDATE_AVATAR=true
|
||||
|
||||
# --- Actions & registre de paquets ---
|
||||
# Le registre est une dépendance vivante de l'infra : stacks/plesna.opytex.org
|
||||
# tire une image depuis cette forge. Explicite pour éviter toute désactivation
|
||||
# par inadvertance.
|
||||
FORGEJO__actions__ENABLED=true
|
||||
FORGEJO__packages__ENABLED=true
|
||||
|
||||
# --- Courriel (relais interne postfix, réseau `smtp`) ---
|
||||
FORGEJO__mailer__ENABLED=true
|
||||
FORGEJO__mailer__PROTOCOL=smtp
|
||||
FORGEJO__mailer__SMTP_ADDR=postfix
|
||||
FORGEJO__mailer__SMTP_PORT=25
|
||||
FORGEJO__mailer__FROM=noreply@opytex.org
|
||||
|
||||
# --- Divers ---
|
||||
FORGEJO__repository__DEFAULT_BRANCH=main
|
||||
FORGEJO__security__INSTALL_LOCK=true
|
||||
75
stacks/forge.opytex.org/runner/config.yaml
Normal file
75
stacks/forge.opytex.org/runner/config.yaml
Normal file
@@ -0,0 +1,75 @@
|
||||
# Configuration du Forgejo Runner (v13).
|
||||
#
|
||||
# Ce fichier est versionné en clair : il ne doit contenir AUCUN secret. Le
|
||||
# jeton du runner est lu dans le volume via `token_url` — voir README.md.
|
||||
#
|
||||
# Les valeurs non mentionnées ici gardent les défauts de l'image. Pour les
|
||||
# revoir : `docker compose run --rm --entrypoint forgejo-runner runner
|
||||
# generate-config`.
|
||||
|
||||
log:
|
||||
# Sortie du processus runner.
|
||||
level: info
|
||||
# Ce qui remonte dans le journal du job, visible dans l'interface web.
|
||||
job_level: info
|
||||
|
||||
runner:
|
||||
# 4 jobs simultanés, comme l'ancien runner. Le défaut de l'image est 1.
|
||||
capacity: 4
|
||||
timeout: 3h
|
||||
fetch_interval: 2s
|
||||
insecure: false
|
||||
|
||||
# Les labels associent un `runs-on` de workflow à une image.
|
||||
# Ces images viennent du registre de Gitea : elles sont conservées telles
|
||||
# quelles parce que les workflows existants supposent leur outillage.
|
||||
# En changer romprait des builds sans prévenir.
|
||||
labels:
|
||||
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
|
||||
- "ubuntu-20.04:docker://docker.gitea.com/runner-images:ubuntu-20.04"
|
||||
# Consommé par les workflows qui déposent sur storage.opytex.org (MinIO).
|
||||
# À revoir le jour du passage à Garage — voir README.md, section
|
||||
# « Dépendance MinIO ».
|
||||
- "minio-mc:docker://minio/mc:latest"
|
||||
|
||||
cache:
|
||||
# Fournit ACTIONS_CACHE_URL aux workflows qui utilisent l'action `cache`.
|
||||
enabled: true
|
||||
# Adresse annoncée aux conteneurs de job pour joindre le proxy de cache.
|
||||
# Vide = détection automatique, correcte tant que le démon Docker tourne sur
|
||||
# la même machine que le runner. C'est ici qu'il faut forcer une IP si
|
||||
# `actions/cache` reste bloqué.
|
||||
host: ""
|
||||
|
||||
container:
|
||||
network: ""
|
||||
privileged: false
|
||||
# Aucun volume montable par les jobs.
|
||||
valid_volumes: []
|
||||
|
||||
# ATTENTION AU SENS DE CETTE VALEUR — il a changé en v13.
|
||||
# "-" ou "" : aucun socket Docker monté dans le conteneur de job
|
||||
# "automount" : socket de l'hôte détecté et monté (défaut d'avant la v13)
|
||||
# `automount` est nécessaire à docker/build-push-action (notytex). En
|
||||
# contrepartie, tout job Actions dispose du démon Docker de l'hôte, donc de
|
||||
# droits équivalents à root sur chioggia.
|
||||
docker_host: "automount"
|
||||
|
||||
# Retire les images à chaque exécution plutôt que de réutiliser une copie
|
||||
# locale. Indispensable ici : le label `minio-mc` pointe sur un tag flottant
|
||||
# (`latest`). Le défaut de l'image est `false`.
|
||||
force_pull: true
|
||||
force_rebuild: false
|
||||
|
||||
server:
|
||||
connections:
|
||||
forgejo:
|
||||
url: https://forge.opytex.org/
|
||||
# L'UUID identifie le runner : ce n'est pas un secret, il tient le rôle
|
||||
# d'un nom d'utilisateur.
|
||||
uuid: 0c25170d-cac2-4342-810e-2880e8833b95
|
||||
# Le jeton, lui, en est un. Il vit dans le volume forgejo_runner_data et
|
||||
# n'apparaît donc jamais dans le dépôt. `token` et `token_url` sont
|
||||
# mutuellement exclusifs : ne pas ajouter `token:` ici.
|
||||
token_url: file:///data/runner-token
|
||||
26
stacks/forge.opytex.org/secrets.env.yaml
Normal file
26
stacks/forge.opytex.org/secrets.env.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
POSTGRES_PASSWORD: ENC[AES256_GCM,data:BucVeYibiQFQWzp7ncTNm/wfCT4x0eTCMCj62CA59xsp1p4XfbVq4phdpkP+cwp+lB/M9m04UiN2l5ievF5Uzg==,iv:I9vk3+/JvF4k3mbpvPqVeC6k2oF2ym9zPvBxN9p1kOA=,tag:m46/+gcDLuALftSJJmQNtQ==,type:str]
|
||||
SSO_CLIENT_SECRET: ENC[AES256_GCM,data:Df+Nppy6tqv5ersu81SKleDQUWq33sIVGsfOn+oiq1jvmmLD7+TFZyTElsPLT/9wOoiHdjGOSqrEKRWW/ZnMrtlxoUmpRXbMntVcLhQIwRZNYa7YptwrLzmkdNqtYsIKYag4MYb2fnu7yqRrOpEwIInrnKhSPwgAlFbYG7yKqfo=,iv:jqDiBGOetJOwlfnhfljjN5Bbga68oaHqcK7O4Pw7fgI=,tag:H0iMVYGqx2IzlWFCM8ETRw==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvMGFWTVZwbVBDZ3NIdGlr
|
||||
MVlGaW1QTGJsUnNHbEpVM2NITXFZTW5aaFZVCklkc2t6S1JlTmhteGNQcjdGOCtT
|
||||
TS8yazMyRjdmdlpGaWVjUVpmLzlYTmsKLS0tICsvM3lkWTVqa1VyTTJINWtPRmp2
|
||||
b2VCY09ITEhCMlhXSzc5VkhtWXZHaWMK15PQvEbBK7fRNfLKMbNhXpcfFWKA/CQE
|
||||
LhE+kymk1XFzB/Ce/+YgP1/ydyZOJK+uN/6+hwBMO82G//Aiep3abQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age18m2zl3pwgw97djshzcf4wmhrqxksl4erqse6edkpxpkg5rcz0ujs8u4c7s
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpRUcwbm5rT00zV3J0TG5K
|
||||
Zm1nNVRSUmtMZU03QTB3ZGJtazQ4SnlnVEVnCjAwNlNIbmtpTVZVNlE5dlhYdHBk
|
||||
VnAveC9KSGx2WUJhb1pGbWJjMTNXeVEKLS0tIHUvcUlmczhrODRwVXhSUzhFN09D
|
||||
ZFNiTlNwYkI1b1c5aGNtZitreGlzeXMKBrYxAUOhDW5o1oQUahpdyb6JkPZ5o/MT
|
||||
w7v9A4Qwe8puT1jgIByZRxAEQfMChmGbrdy1PHoeq7DjKYayDRu8fA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1fh70nny5hzz8a8g9077kgfca0lhlh3wsjf4fjrv4klx5ucdtpdmsazuqdj
|
||||
lastmodified: "2026-08-20T13:09:10Z"
|
||||
mac: ENC[AES256_GCM,data:VDaU4oyWyIF6SeL8soPV46Aqc6aPaU3q9dZtSQDNV8hHqb7H8SLqS15Q3WacPXdO8Ee+jSydXcTf5wMENsOd2XqNoz+ClKHGZpbyglHFKOA8pNBpGnzBZ1MYgJgEB2BHal/UC17Z5J3uxhGKHD5HDyYTkQLwXkMnvoRJB74k4io=,iv:6hdeZcMpw3Pd8ZPT5eoZPOe3+QLHTJJWUMZRELuQ9iI=,tag:jhEc0vKGd7MQYq9dUNyjiA==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
96
stacks/mail-relay/README.md
Normal file
96
stacks/mail-relay/README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# mail-relay — relais SMTP interne
|
||||
|
||||
Postfix en mode *satellite* : il n'accepte du courrier que depuis le réseau Docker
|
||||
`smtp` et le réexpédie au relais du fournisseur, authentifié avec un compte d'envoi.
|
||||
|
||||
Pas de FQDN, pas de routeur Traefik, pas de port publié, pas de volume : le dossier
|
||||
porte donc le nom du service et non un domaine, comme `stacks/traefik/`.
|
||||
|
||||
## Contrat avec les autres stacks — à ne pas casser
|
||||
|
||||
| Élément | Valeur | Pourquoi c'est figé |
|
||||
|---|---|---|
|
||||
| Réseau Docker | `smtp` | déclaré `external: true` par tous les consommateurs |
|
||||
| Nom d'hôte | `postfix` | valeur de `SMTP_HOST` chez les consommateurs |
|
||||
| Port | `25` | sans TLS ni authentification, réseau interne uniquement |
|
||||
|
||||
Le nom d'hôte `postfix` vient du **nom de service** Compose, pas du nom de projet :
|
||||
renommer le dossier ne le change pas. Renommer le *service*, en revanche, casserait
|
||||
tous les consommateurs en silence — les mails cesseraient de partir sans qu'aucun
|
||||
conteneur ne tombe.
|
||||
|
||||
Consommateurs : `vault.opytex.org`, `authentik`, `matrix_poneyworld`.
|
||||
|
||||
## Le réseau `smtp` est externe
|
||||
|
||||
Il n'est pas créé par cette stack, pour qu'un `docker compose down` ici ne tente pas
|
||||
de supprimer un réseau auquel d'autres stacks sont attachées. À créer une seule fois,
|
||||
s'il n'existe pas déjà :
|
||||
|
||||
```bash
|
||||
docker network ls --filter name=^smtp$ --format '{{.Name}}'
|
||||
docker network create smtp # seulement si la commande ci-dessus ne renvoie rien
|
||||
```
|
||||
|
||||
## Déploiement
|
||||
|
||||
```bash
|
||||
cd ~/infra/stacks/mail-relay
|
||||
sops exec-env secrets.env.yaml 'docker compose up -d'
|
||||
```
|
||||
|
||||
Après une modification d'un secret, `up -d` répond « up to date » et garde l'ancienne
|
||||
valeur. Il faut forcer :
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml 'docker compose up -d --force-recreate'
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`env_file` porte le relais amont et l'identité annoncée (`SMTP_SERVER`, `SMTP_PORT`,
|
||||
`SERVER_HOSTNAME`, `OVERWRITE_FROM`) ; `secrets.env.yaml` porte les identifiants du
|
||||
compte d'envoi (`SMTP_USERNAME`, `SMTP_PASSWORD`), édités avec `sops edit`.
|
||||
|
||||
Les noms sont ceux que l'image lit directement. Pour voir ce que le conteneur a
|
||||
réellement reçu :
|
||||
|
||||
```bash
|
||||
docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' postfix \
|
||||
| grep -E 'SMTP|SERVER|OVERWRITE'
|
||||
```
|
||||
|
||||
Un `SMTP_USERNAME` ou `SMTP_PASSWORD` vide signale un `sops exec-env` oublié.
|
||||
|
||||
## Vérifier que le relais fonctionne
|
||||
|
||||
```bash
|
||||
# le nom d'hôte résout depuis un consommateur
|
||||
docker exec vaultwarden getent hosts postfix
|
||||
|
||||
# envoi de bout en bout
|
||||
docker exec -i postfix sh -c 'sendmail -t' <<'EOF'
|
||||
To: destinataire@example.org
|
||||
Subject: test relais
|
||||
|
||||
test
|
||||
EOF
|
||||
|
||||
# file d'attente et journal
|
||||
docker exec postfix mailq
|
||||
docker logs --tail 50 postfix
|
||||
```
|
||||
|
||||
Une file d'attente qui ne se vide pas, ou un `status=deferred` dans le journal, pointe
|
||||
l'authentification auprès du relais amont.
|
||||
|
||||
## Points d'attention
|
||||
|
||||
- **La file d'attente n'est pas persistée.** `/var/spool/postfix` vit dans la couche
|
||||
d'écriture du conteneur : un `docker compose up --force-recreate` perd les messages
|
||||
non encore remis. Acceptable pour des notifications, à revoir si le relais transporte
|
||||
un jour du courrier qui compte.
|
||||
- **Aucune sauvegarde.** Pas de volume, rien à restaurer : la stack se reconstruit
|
||||
entièrement depuis ce dossier plus les secrets.
|
||||
- Le relais fait confiance à tout ce qui est joint au réseau `smtp`. Ne pas y attacher
|
||||
un service exposé qui n'a pas besoin d'envoyer du courrier.
|
||||
22
stacks/mail-relay/docker-compose.yml
Normal file
22
stacks/mail-relay/docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
postfix:
|
||||
image: juanluisbaptiste/postfix:1.9.0
|
||||
container_name: postfix
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
env_file:
|
||||
- env_file
|
||||
environment:
|
||||
SMTP_USERNAME: ${SMTP_USERNAME:?}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD:?}
|
||||
networks:
|
||||
- smtp
|
||||
|
||||
networks:
|
||||
# Créé hors stack (`docker network create smtp`) et déclaré `external` par
|
||||
# tous les consommateurs. Le garder externe ici aussi évite qu'un
|
||||
# `docker compose down` sur ce relais tente de supprimer un réseau auquel
|
||||
# d'autres stacks sont attachées.
|
||||
smtp:
|
||||
external: true
|
||||
21
stacks/mail-relay/env_file
Normal file
21
stacks/mail-relay/env_file
Normal file
@@ -0,0 +1,21 @@
|
||||
# =============================================================================
|
||||
# MAIL-RELAY (postfix) — configuration NON sensible
|
||||
# =============================================================================
|
||||
# Relais SMTP interne de l'infra. Aucun port publié, aucun routeur Traefik :
|
||||
# il n'est joignable que sur le réseau Docker `smtp`, sous le nom d'hôte
|
||||
# `postfix`, port 25, sans authentification ni TLS.
|
||||
#
|
||||
# Les noms de variables ci-dessous sont ceux attendus par l'image
|
||||
# juanluisbaptiste/postfix, consommés directement.
|
||||
#
|
||||
# SERVER_HOSTNAME : nom annoncé dans le HELO/EHLO, doit correspondre au SPF.
|
||||
# OVERWRITE_FROM : réécrit l'expéditeur de tous les messages relayés.
|
||||
#
|
||||
# Les identifiants du compte d'envoi (SMTP_USERNAME, SMTP_PASSWORD) sont dans
|
||||
# secrets.env.yaml et injectés par `sops exec-env`.
|
||||
# =============================================================================
|
||||
|
||||
SMTP_SERVER=smtp.mailfence.com
|
||||
SMTP_PORT=465
|
||||
SERVER_HOSTNAME=opytex.org
|
||||
OVERWRITE_FROM=noreply@opytex.org
|
||||
26
stacks/mail-relay/secrets.env.yaml
Normal file
26
stacks/mail-relay/secrets.env.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
SMTP_USERNAME: ENC[AES256_GCM,data:+7Kp5g9BMDo=,iv:HwSc6EfVkaipNbEKsdSYKKqu2zmKVdfGdwxmF3DeBcU=,tag:HPdOovDuvWh9+5OUep61HQ==,type:str]
|
||||
SMTP_PASSWORD: ENC[AES256_GCM,data:nkxxgMlpwjoauzF9,iv:aMB9u7i5zVOoZaCCndiV+h43oxT2FiN+uTgo6fU/6/c=,tag:HE6V9Xtly6vcnWIM4fAtIQ==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBueXNlTFdaRWxHUjBvU0Nv
|
||||
MFRJQXZobURqdWRJK0dLSmp2WEx5NDMyWFhnCnp5a1ZIN2pSbHQ2aDBVdjV1UUVu
|
||||
MWNqQjloQXV4QUZoVVZDeUJvMUkvQ00KLS0tIDdSeVA2b1FqVWhHeDdrTDV5bXZ4
|
||||
T3ZIcVlGdUp1cXBETWxTRTZuVzVUdkEKFiEf3srsmNQND/dAftYWZWhzBZEVJDVN
|
||||
C7sWjY+VoaUu/kLmkpSkvlU1AkMyiHuIGbFJcZVWkTo7/+nfDqd60Q==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age18m2zl3pwgw97djshzcf4wmhrqxksl4erqse6edkpxpkg5rcz0ujs8u4c7s
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXTXM5S0M5cE1mZXJqdWlC
|
||||
ckx4QVE3YkplUkdUcjBDVHJMZ1lCUHJ0N0Q4CkZyUG9PclNQS1FTbTJySG1WQkRG
|
||||
Vm1RdE0wN1BDc0YwdUtmYzBneGZHWVEKLS0tIDg5NzczYkR5YkpldUFtNHRUekxk
|
||||
eUxHY2NTM09kRXQ1L3pkd3MwTlhTU0UKlNWwpdlqqwo7r3+gUZN4GJ3Qarr0DkFS
|
||||
XUJApSBn8vYCG02BRGDBkKlm6KdqspHl36fgsB2pmtZm+oTIhG+1OQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1fh70nny5hzz8a8g9077kgfca0lhlh3wsjf4fjrv4klx5ucdtpdmsazuqdj
|
||||
lastmodified: "2026-08-17T04:51:05Z"
|
||||
mac: ENC[AES256_GCM,data:Eoha31ZU3Rh4+BynONCTvz8/2PWcJ2bb8FQN1DlADnay2RcNKTa2rKkdipINLmVsMDV6BlWj+4RgQP2aVEL7XJmMVPjNKlun6tMClsT69NvJIYvlw9HbfEpXdw8U8SsNNATB2cyQ9Imayu+Zb2IWa/4GnzUUfe3PQai3rih3Yt8=,iv:0Ejixjh4A0Op/0+UpQdcsXktwwtXQoweYrPEqfVQwB0=,tag:ln4E7z6LJX2QV8rGTXIW/A==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
maintenant:
|
||||
image: ghcr.io/kolapsis/maintenant:1.3.5
|
||||
image: ghcr.io/kolapsis/maintenant:1.3.9
|
||||
container_name: maintenant
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
|
||||
233
stacks/matrix.poneyworld.net/README.md
Normal file
233
stacks/matrix.poneyworld.net/README.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# Matrix — matrix.poneyworld.net
|
||||
|
||||
Homeserver Synapse et ses satellites. Huit conteneurs :
|
||||
|
||||
| Service | Rôle | Exposé sur |
|
||||
|---|---|---|
|
||||
| `synapse` | le homeserver | `synapse.poneyworld.net` (fédération) |
|
||||
| `db` | PostgreSQL 18 | — |
|
||||
| `redis` | Valkey, cache interne de Synapse (bloc `redis:` du homeserver) | — |
|
||||
| `nginx` | sert `/_matrix` (vers `synapse:8008`) et `/.well-known/matrix/` | `matrix.poneyworld.net` (API client) |
|
||||
| `web` | Element Web | `chat.poneyworld.net` |
|
||||
| `livekit` | SFU pour Element Call | `livekit.opytex.org` |
|
||||
| `lk-jwt` | délivre les jetons LiveKit à Element | `livekit.opytex.org/sfu/get`, `/get_token`, `/healthz` |
|
||||
| `coturn` | serveur TURN (appels 1-à-1) | ports 3478 / 5349 / 49160-49200 |
|
||||
|
||||
Deux entrées différentes, à ne pas confondre : `matrix.poneyworld.net` est le
|
||||
`server_name` et l'API client, `synapse.poneyworld.net` est la cible de
|
||||
délégation annoncée par `/.well-known/matrix/server`, empruntée par les
|
||||
serveurs fédérés. Les deux sont vivantes.
|
||||
|
||||
## Déploiement
|
||||
|
||||
Depuis ce dossier, sur chioggia :
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml 'docker compose up -d'
|
||||
```
|
||||
|
||||
Les secrets sont déclarés `${VAR:?}` : **toute** commande `docker compose`
|
||||
échoue sans l'enveloppe `sops exec-env`, y compris `stop`, `logs`, `ps` et
|
||||
`exec`. Pour en enchaîner plusieurs, une seule enveloppe suffit :
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml '
|
||||
docker compose stop synapse &&
|
||||
docker compose exec -T db psql -U synapse synapse -c "..." &&
|
||||
docker compose up -d
|
||||
'
|
||||
```
|
||||
|
||||
Après une rotation de secret, `docker compose up -d` répond « up to date » et
|
||||
garde l'ancienne valeur. Il faut `--force-recreate` sur le service concerné.
|
||||
|
||||
## Le nom de projet
|
||||
|
||||
`name: matrix_poneyworld`, en tête du compose, ne suit pas le nom du dossier.
|
||||
C'est délibéré et c'est porteur : les volumes s'appellent
|
||||
`matrix_poneyworld_matrix_data` et `matrix_poneyworld_matrix_db`.
|
||||
|
||||
**Retirer cette ligne, ou la modifier, rendrait invisibles les médias, les
|
||||
clés de signature du serveur, `homeserver.yaml` et toute la base — la stack
|
||||
repartirait sur un homeserver vide, sans la moindre erreur.**
|
||||
|
||||
## Où vit vraiment la configuration
|
||||
|
||||
Presque rien de la configuration de Synapse n'est dans ce dépôt : elle est
|
||||
dans `/data/homeserver.yaml`, à l'intérieur du volume `matrix_data`.
|
||||
|
||||
Les variables `SYNAPSE_*` de l'`env_file` ne sont lues que par le mode
|
||||
`generate` de l'image, au tout premier démarrage. Elles sont **inertes au
|
||||
runtime** : `SYNAPSE_ENABLE_REGISTRATION=yes` y cohabite sans conséquence avec
|
||||
un `enable_registration: False` effectif dans `homeserver.yaml`. Ne pas s'y
|
||||
fier pour lire l'état du serveur.
|
||||
|
||||
Corollaire pour les secrets : `turn_shared_secret`, `macaroon_secret_key`,
|
||||
`registration_shared_secret` et le mot de passe de la base sont dans
|
||||
`homeserver.yaml`, donc **hors SOPS et hors dépôt**. Une rotation se fait des
|
||||
deux côtés (voir plus bas).
|
||||
|
||||
## Secrets
|
||||
|
||||
Cinq valeurs dans `secrets.env.yaml`, injectées par `sops exec-env` :
|
||||
|
||||
| Variable | Consommé par |
|
||||
|---|---|
|
||||
| `POSTGRES_PASSWORD` | `db` |
|
||||
| `LIVEKIT_KEY`, `LIVEKIT_SECRET` | `livekit` (via `LIVEKIT_KEYS`) et `lk-jwt` |
|
||||
| `TURN_STATIC_AUTH_SECRET`, `TURN_CLI_PASSWORD` | `coturn` |
|
||||
|
||||
`LIVEKIT_KEY` tient plus de l'identifiant que du secret. Il est chiffré avec
|
||||
son secret parce que les deux se tournent toujours ensemble : les séparer sur
|
||||
deux mécanismes rendrait la rotation facile à ne faire qu'à moitié.
|
||||
|
||||
```bash
|
||||
sops edit secrets.env.yaml
|
||||
```
|
||||
|
||||
### Deux mécanismes différents pour deux logiciels
|
||||
|
||||
Ni coturn ni LiveKit ne lisent une variable d'environnement depuis leur
|
||||
fichier de configuration — un fichier monté n'est pas interpolé par Compose.
|
||||
C'est le même problème que la clé LAPI de CrowdSec sur Traefik, mais les deux
|
||||
n'offrent pas la même sortie.
|
||||
|
||||
**LiveKit** accepte la paire par variable d'environnement : `LIVEKIT_KEYS`, au
|
||||
format `clé: secret`. `livekit.yaml` n'a donc pas de bloc `keys:`, et la
|
||||
valeur est composée dans `environment:` du compose, où l'interpolation
|
||||
`${...}` s'applique. `lk-jwt` consomme les deux mêmes variables.
|
||||
|
||||
**coturn** n'offre rien d'équivalent : pas d'interpolation dans
|
||||
`turnserver.conf`, pas d'option `...File`, pas de lecture d'environnement. La
|
||||
seule voie sans bricolage est la ligne de commande :
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- -c
|
||||
- /etc/coturn/turnserver.conf
|
||||
- --static-auth-secret=${TURN_STATIC_AUTH_SECRET:?}
|
||||
- --cli-password=${TURN_CLI_PASSWORD:?}
|
||||
```
|
||||
|
||||
Le compromis est là : les deux valeurs sont visibles dans `docker inspect
|
||||
coturn` et dans la table des processus de l'hôte. Un secret Compose ferait
|
||||
mieux, coturn ne sait pas le lire.
|
||||
|
||||
`turnserver.conf` ne doit jamais porter de ligne `static-auth-secret=` ou
|
||||
`cli-password=` : ce serait un secret en clair dans le dépôt, et une seconde
|
||||
source concurrente de celle du `command:`.
|
||||
|
||||
### Rotation
|
||||
|
||||
`turn_shared_secret` ne se change **pas seul** : la même valeur doit être
|
||||
identique dans `homeserver.yaml` (Synapse la signe) et côté coturn (il la
|
||||
vérifie). Deux valeurs différentes ne produisent aucune erreur au démarrage —
|
||||
les appels échouent simplement à établir un relais.
|
||||
|
||||
```bash
|
||||
sops edit secrets.env.yaml # TURN_STATIC_AUTH_SECRET
|
||||
|
||||
# homeserver.yaml s'édite depuis l'hôte : l'image Synapse n'a pas d'éditeur.
|
||||
sudo vi "$(docker volume inspect matrix_poneyworld_matrix_data \
|
||||
--format '{{.Mountpoint}}')/homeserver.yaml" # turn_shared_secret
|
||||
|
||||
sops exec-env secrets.env.yaml '
|
||||
docker compose up -d --force-recreate coturn &&
|
||||
docker compose restart synapse
|
||||
'
|
||||
```
|
||||
|
||||
`macaroon_secret_key` (dans `homeserver.yaml`) signe les jetons d'accès : le
|
||||
changer **déconnecte tous les utilisateurs**, sur tous leurs appareils. Ce
|
||||
n'est pas une opération anodine, c'est une opération annoncée.
|
||||
|
||||
`registration_shared_secret` (dans `homeserver.yaml`) n'est utilisé que par
|
||||
`register_new_matrix_user`. Il se change librement.
|
||||
|
||||
`POSTGRES_PASSWORD` ne se change pas seul non plus : PostgreSQL ignore la
|
||||
variable sur un `PGDATA` déjà initialisé. Il faut un `ALTER USER` dans la base
|
||||
**et** la même valeur dans le bloc `database:` de `homeserver.yaml`.
|
||||
|
||||
## Volumes et sauvegarde
|
||||
|
||||
| Volume | Contenu | Sauvegarde |
|
||||
|---|---|---|
|
||||
| `matrix_poneyworld_matrix_data` | médias, clé de signature du serveur, `homeserver.yaml` | label `backup.enable=true` + `MANUAL_VOLUMES` |
|
||||
| `matrix_poneyworld_matrix_db` | base PostgreSQL | dump SQL (`backup.type=postgres` sur `db`) |
|
||||
|
||||
Les labels de volume ne s'appliquent qu'à la création : en ajouter ou en
|
||||
retirer un ne changerait rien au volume réel, mais ferait diverger sa
|
||||
déclaration, et Compose proposerait alors de le **recréer**. Ne pas toucher au
|
||||
bloc `volumes:` du compose.
|
||||
|
||||
### `POSTGRES_DB` conditionne le dump
|
||||
|
||||
`backup_docker.sh` lit la base à dumper dans l'environnement du conteneur
|
||||
`db` :
|
||||
|
||||
```bash
|
||||
db_name=$(docker exec "$container" printenv POSTGRES_DB 2>/dev/null || echo "postgres")
|
||||
```
|
||||
|
||||
Sans `POSTGRES_DB`, le repli s'applique : le script dumpe la base système
|
||||
`postgres`, vide, et journalise un succès. `POSTGRES_DB=synapse` dans
|
||||
l'`env_file` n'a pas d'autre fonction que d'être visible par ce `printenv` —
|
||||
PostgreSQL, lui, ne lit cette variable qu'à l'initialisation du cluster.
|
||||
|
||||
Vérification, après tout changement touchant `db` :
|
||||
|
||||
```bash
|
||||
zcat /other/backups/db-dumps/matrix_poneyworld.sql.gz | grep -c "CREATE TABLE"
|
||||
```
|
||||
|
||||
Un `0` signifie que la sauvegarde est vide.
|
||||
|
||||
## L'IP publique est écrite en dur, à deux endroits
|
||||
|
||||
`coturn/turnserver.conf` (`external-ip`) et `livekit/livekit.yaml` (`node_ip`)
|
||||
portent tous deux l'adresse publique de chioggia, `188.165.240.182`. Les deux
|
||||
services tournent en bridge : ils ne voient qu'une IP privée et ne peuvent pas
|
||||
deviner celle qu'il faut annoncer aux clients.
|
||||
|
||||
**Si la machine change d'adresse, ces deux fichiers doivent changer avec
|
||||
elle.** Aucun des deux ne le signalera : coturn et LiveKit démarrent
|
||||
normalement en annonçant une adresse erronée, et seuls les appels échouent —
|
||||
sans erreur dans les logs, puisque du point de vue des serveurs tout va bien.
|
||||
|
||||
## Le chemin des requêtes clientes
|
||||
|
||||
`matrix.poneyworld.net` passe par Traefik, puis nginx, puis Synapse. Deux
|
||||
conséquences à ne pas défaire :
|
||||
|
||||
- nginx doit propager la chaîne avec `$proxy_add_x_forwarded_for`, jamais
|
||||
`$remote_addr` : ce dernier vaut l'adresse de **Traefik**, et l'utiliser
|
||||
écrase l'en-tête posé en amont. Synapse voit alors tous les clients sous une
|
||||
seule adresse — la limitation de débit à la connexion devient un compteur
|
||||
partagé par tous les comptes, et les journaux d'authentification perdent leur
|
||||
intérêt. Côté Synapse, le listener 8008 doit porter `x_forwarded: true`.
|
||||
- `crowdsec@file` est posé sur les routeurs `matrix_nginx` (API client) et
|
||||
`element` (interface web), **délibérément pas sur `synapse`** : ce dernier
|
||||
sert la fédération, qui arrive de milliers de serveurs distants. Une décision
|
||||
de bannissement à cet endroit couperait les correspondants.
|
||||
|
||||
Contrôle de la chaîne, une seule adresse en `172.x` signalant le problème :
|
||||
|
||||
```sql
|
||||
SELECT ip, count(*) FROM user_ips GROUP BY ip ORDER BY count DESC LIMIT 10;
|
||||
```
|
||||
|
||||
## Versions d'images
|
||||
|
||||
Toutes les images sont épinglées au patch. Les tags d'amont ne suivent pas la
|
||||
même forme : `livekit-server` préfixe d'un `v`, `lk-jwt-service` non — bien
|
||||
que son tag *git* le porte. Le label OCI
|
||||
`org.opencontainers.image.version` du conteneur donne la forme du tag d'image,
|
||||
c'est lui qui fait foi.
|
||||
|
||||
`livekit` et `lk-jwt` se montent ensemble : ils s'authentifient mutuellement et
|
||||
suivent le même protocole MatrixRTC.
|
||||
|
||||
Références :
|
||||
|
||||
- https://element-hq.github.io/synapse/latest/setup/turn/coturn.html
|
||||
- https://docs.livekit.io/home/self-hosting/deployment/
|
||||
14
stacks/matrix.poneyworld.net/coturn/turnserver.conf
Normal file
14
stacks/matrix.poneyworld.net/coturn/turnserver.conf
Normal file
@@ -0,0 +1,14 @@
|
||||
# Serveur TURN de matrix.poneyworld.net.
|
||||
#
|
||||
# `static-auth-secret` et `cli-password` ne sont PAS ici : coturn n'interpole
|
||||
# aucune variable dans ce fichier et n'a pas d'option `...File`. Ils sont
|
||||
# passés en `command:` par docker-compose.yml, alimentés par SOPS.
|
||||
use-auth-secret
|
||||
realm=matrix.poneyworld.net
|
||||
listening-port=3478
|
||||
tls-listening-port=5349
|
||||
min-port=49160
|
||||
max-port=49200
|
||||
verbose
|
||||
allow-loopback-peers
|
||||
external-ip=188.165.240.182
|
||||
209
stacks/matrix.poneyworld.net/docker-compose.yml
Normal file
209
stacks/matrix.poneyworld.net/docker-compose.yml
Normal file
@@ -0,0 +1,209 @@
|
||||
# Le nom de projet ne suit PAS le dossier : les volumes existants sont
|
||||
# préfixés `matrix_poneyworld_`. Retirer cette ligne ferait repartir Synapse
|
||||
# sur une base et un magasin de médias vides.
|
||||
name: matrix_poneyworld
|
||||
|
||||
services:
|
||||
synapse:
|
||||
image: matrixdotorg/synapse:v1.159.0
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
# Forme longue : `depends_on` court n'ordonne que le DÉMARRAGE, sans
|
||||
# attendre que la base accepte les connexions.
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- env_file
|
||||
volumes:
|
||||
- matrix_data:/data
|
||||
networks:
|
||||
- default
|
||||
- traefik-proxy
|
||||
- smtp
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-proxy
|
||||
- traefik.http.routers.synapse.entrypoints=web-secure
|
||||
- traefik.http.routers.synapse.rule=Host(`synapse.poneyworld.net`)
|
||||
- traefik.http.routers.synapse.tls.certresolver=letsencrypt
|
||||
- traefik.http.services.synapse.loadbalancer.server.port=8008
|
||||
|
||||
db:
|
||||
image: postgres:18.6
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
env_file:
|
||||
- env_file
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
|
||||
POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C
|
||||
volumes:
|
||||
- matrix_db:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||
interval: 15s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
start_period: 30s
|
||||
networks:
|
||||
- default
|
||||
labels:
|
||||
backup.enable: true
|
||||
backup.type: postgres
|
||||
|
||||
redis:
|
||||
image: valkey/valkey:9.1.1
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
healthcheck:
|
||||
test: ["CMD", "valkey-cli", "ping"]
|
||||
interval: 15s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
networks:
|
||||
- default
|
||||
|
||||
nginx:
|
||||
image: nginx:1.30.4
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
# `proxy_pass http://synapse:8008` est résolu au CHARGEMENT de la config :
|
||||
# sans ce depends_on, nginx s'arrête sur « host not found in upstream » au
|
||||
# démarrage de la machine et ne revient que par la boucle `restart`.
|
||||
depends_on:
|
||||
synapse:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./nginx/matrix.conf:/etc/nginx/conf.d/matrix.conf:ro
|
||||
- ./nginx/www:/var/www/:ro
|
||||
networks:
|
||||
- default
|
||||
- traefik-proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-proxy
|
||||
- traefik.http.routers.matrix_nginx.entrypoints=web-secure
|
||||
- traefik.http.routers.matrix_nginx.rule=Host(`matrix.poneyworld.net`)
|
||||
- traefik.http.routers.matrix_nginx.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.matrix_nginx.middlewares=crowdsec@file
|
||||
- traefik.http.services.matrix_nginx.loadbalancer.server.port=80
|
||||
|
||||
web:
|
||||
image: vectorim/element-web:v1.12.26
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
volumes:
|
||||
- ./element/config.json:/app/config.json:ro
|
||||
networks:
|
||||
- default
|
||||
- traefik-proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-proxy
|
||||
- traefik.http.routers.element.entrypoints=web-secure
|
||||
- traefik.http.routers.element.rule=Host(`chat.poneyworld.net`)
|
||||
- traefik.http.routers.element.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.element.middlewares=crowdsec@file
|
||||
- traefik.http.services.element.loadbalancer.server.port=80
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:v1.13.5
|
||||
command: --config /etc/livekit.yaml
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
environment:
|
||||
# `keys:` a été retiré de livekit.yaml : la paire arrive ici, où
|
||||
# l'interpolation ${...} de Compose s'applique.
|
||||
LIVEKIT_KEYS: "${LIVEKIT_KEY:?}: ${LIVEKIT_SECRET:?}"
|
||||
volumes:
|
||||
- ./livekit/livekit.yaml:/etc/livekit.yaml:ro
|
||||
ports:
|
||||
- "7881:7881/tcp"
|
||||
- "50100-50200:50100-50200/udp"
|
||||
networks:
|
||||
- default
|
||||
- traefik-proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-proxy
|
||||
- traefik.http.routers.livekit.entrypoints=web-secure
|
||||
- traefik.http.routers.livekit.rule=Host(`livekit.opytex.org`)
|
||||
- traefik.http.routers.livekit.tls.certresolver=letsencrypt
|
||||
- traefik.http.services.livekit.loadbalancer.server.port=7880
|
||||
|
||||
lk-jwt:
|
||||
image: ghcr.io/element-hq/lk-jwt-service:0.6.0
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
depends_on:
|
||||
- livekit
|
||||
environment:
|
||||
# Pas de LIVEKIT_JWT_BIND : le healthcheck de l'image assemble
|
||||
# `http://localhost:` + la valeur, ce qui donne un double deux-points et
|
||||
# le fait échouer. Le défaut (:8080) est le seul réglage qui fonctionne.
|
||||
LIVEKIT_URL: wss://livekit.opytex.org
|
||||
LIVEKIT_KEY: ${LIVEKIT_KEY:?}
|
||||
LIVEKIT_SECRET: ${LIVEKIT_SECRET:?}
|
||||
LIVEKIT_FULL_ACCESS_HOMESERVERS: matrix.poneyworld.net
|
||||
networks:
|
||||
- default
|
||||
- traefik-proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-proxy
|
||||
- traefik.http.routers.lkjwt.entrypoints=web-secure
|
||||
- traefik.http.routers.lkjwt.rule=Host(`livekit.opytex.org`) && (PathPrefix(`/sfu/get`) || PathPrefix(`/get_token`) || PathPrefix(`/healthz`))
|
||||
- traefik.http.routers.lkjwt.tls.certresolver=letsencrypt
|
||||
- traefik.http.services.lkjwt.loadbalancer.server.port=8080
|
||||
|
||||
coturn:
|
||||
image: coturn/coturn:4.17.2
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
# Les deux secrets ne peuvent pas venir du fichier de conf : coturn n'y
|
||||
# interpole rien et n'offre aucune option `...File`. Voir README.md.
|
||||
command:
|
||||
- -c
|
||||
- /etc/coturn/turnserver.conf
|
||||
- --static-auth-secret=${TURN_STATIC_AUTH_SECRET:?}
|
||||
- --cli-password=${TURN_CLI_PASSWORD:?}
|
||||
volumes:
|
||||
- ./coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro
|
||||
# `verbose` dans turnserver.conf journalise chaque allocation et chaque
|
||||
# permission : sans borne, le fichier json croît sans fin.
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "1m"
|
||||
# 3478 doit être publié en UDP autant qu'en TCP : un client TURN tente
|
||||
# l'UDP en premier. 5349 (TURNS) reste en TCP seul, c'est du TLS.
|
||||
ports:
|
||||
- "3478:3478"
|
||||
- "3478:3478/udp"
|
||||
- "5349:5349"
|
||||
- "49160-49200:49160-49200/udp"
|
||||
networks:
|
||||
- default
|
||||
|
||||
volumes:
|
||||
matrix_data:
|
||||
labels:
|
||||
backup.enable: true
|
||||
matrix_db:
|
||||
|
||||
networks:
|
||||
traefik-proxy:
|
||||
external: true
|
||||
smtp:
|
||||
external: true
|
||||
65
stacks/matrix.poneyworld.net/element/config.json
Normal file
65
stacks/matrix.poneyworld.net/element/config.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"default_server_config": {
|
||||
"m.homeserver": {
|
||||
"base_url": "https://matrix.poneyworld.net",
|
||||
"server_name": "matrix.poneyworld.net"
|
||||
},
|
||||
"m.identity_server": {
|
||||
"base_url": "https://vector.im"
|
||||
}
|
||||
},
|
||||
"disable_custom_urls": false,
|
||||
"disable_guests": false,
|
||||
"disable_login_language_selector": false,
|
||||
"disable_3pid_login": false,
|
||||
"brand": "Element",
|
||||
"integrations_ui_url": "https://scalar.vector.im/",
|
||||
"integrations_rest_url": "https://scalar.vector.im/api",
|
||||
"integrations_widgets_urls": [
|
||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||
"https://scalar.vector.im/api",
|
||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||
"https://scalar-staging.vector.im/api",
|
||||
"https://scalar-staging.riot.im/scalar/api"
|
||||
],
|
||||
"bug_report_endpoint_url": "https://riot.im/bugreports/submit",
|
||||
"defaultCountryCode": "FR",
|
||||
"showLabsSettings": false,
|
||||
"features": {
|
||||
"feature_new_spinner": "labs",
|
||||
"feature_pinning": "labs",
|
||||
"feature_custom_status": "labs",
|
||||
"feature_custom_tags": "labs",
|
||||
"feature_state_counters": "labs"
|
||||
},
|
||||
"default_federate": true,
|
||||
"default_theme": "light",
|
||||
"roomDirectory": {
|
||||
"servers": [
|
||||
"matrix.org"
|
||||
]
|
||||
},
|
||||
"welcomeUserId": "@riot-bot:matrix.org",
|
||||
"piwik": {
|
||||
"url": "https://piwik.riot.im/",
|
||||
"whitelistedHSUrls": [
|
||||
"https://matrix.org"
|
||||
],
|
||||
"whitelistedISUrls": [
|
||||
"https://vector.im",
|
||||
"https://matrix.org"
|
||||
],
|
||||
"siteId": 1
|
||||
},
|
||||
"enable_presence_by_hs_url": {
|
||||
"https://matrix.org": false,
|
||||
"https://matrix-client.matrix.org": false
|
||||
},
|
||||
"settingDefaults": {
|
||||
"breadcrumbs": true
|
||||
},
|
||||
"element_call": {
|
||||
"url": "https://call.element.io",
|
||||
"use_exclusively": true
|
||||
}
|
||||
}
|
||||
21
stacks/matrix.poneyworld.net/env_file
Normal file
21
stacks/matrix.poneyworld.net/env_file
Normal file
@@ -0,0 +1,21 @@
|
||||
# Synapse — configuration non sensible.
|
||||
#
|
||||
# La configuration réelle du homeserver n'est PAS ici : elle vit dans
|
||||
# /data/homeserver.yaml, à l'intérieur du volume matrix_data, hors dépôt.
|
||||
# Les variables SYNAPSE_* ci-dessous ne sont lues que par le mode `generate`
|
||||
# de l'image, au tout premier démarrage. Elles sont inertes au runtime :
|
||||
# SYNAPSE_ENABLE_REGISTRATION=yes cohabite ainsi avec un
|
||||
# `enable_registration: False` effectif dans homeserver.yaml.
|
||||
SYNAPSE_HOST=synapse.poneyworld.net
|
||||
SYNAPSE_NO_TLS=true
|
||||
SYNAPSE_REPORT_STATS=no
|
||||
SYNAPSE_ENABLE_REGISTRATION=yes
|
||||
PROTOCOL=http
|
||||
|
||||
# Lues par le conteneur `db`, et par le script de sauvegarde qui fait
|
||||
# `docker exec printenv POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD`.
|
||||
# Sans POSTGRES_DB, le script retombe sur `postgres` et dumpe une base vide
|
||||
# en signalant un succès. Voir README.md.
|
||||
# POSTGRES_PASSWORD n'est pas ici, il vient de SOPS.
|
||||
POSTGRES_USER=synapse
|
||||
POSTGRES_DB=synapse
|
||||
13
stacks/matrix.poneyworld.net/livekit/livekit.yaml
Normal file
13
stacks/matrix.poneyworld.net/livekit/livekit.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
port: 7880
|
||||
bind_addresses:
|
||||
- "0.0.0.0"
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
# udp_port: 7882
|
||||
port_range_start: 50100
|
||||
port_range_end: 50200
|
||||
#use_external_ip: true
|
||||
enable_loopback_candidate: false
|
||||
node_ip: 188.165.240.182
|
||||
# `keys:` est absent volontairement : la paire clé/secret arrive par la
|
||||
# variable d'environnement LIVEKIT_KEYS, alimentée par SOPS.
|
||||
20
stacks/matrix.poneyworld.net/nginx/matrix.conf
Normal file
20
stacks/matrix.poneyworld.net/nginx/matrix.conf
Normal file
@@ -0,0 +1,20 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name matrix.poneyworld.net;
|
||||
|
||||
# Traefik -> nginx -> synapse
|
||||
location /_matrix {
|
||||
proxy_pass http://synapse:8008;
|
||||
# $remote_addr vaut ici l'adresse de Traefik, pas celle du client : l'utiliser
|
||||
# ÉCRASE l'en-tête posé par Traefik et rend tous les clients identiques aux
|
||||
# yeux de Synapse. $proxy_add_x_forwarded_for complète la chaîne existante.
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
client_max_body_size 128m;
|
||||
}
|
||||
|
||||
location /.well-known/matrix/ {
|
||||
root /var/www/;
|
||||
default_type application/json;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"m.homeserver": {
|
||||
"base_url": "https://matrix.poneyworld.net"
|
||||
},
|
||||
"org.matrix.msc4143.rtc_foci": [
|
||||
{
|
||||
"type": "livekit",
|
||||
"livekit_service_url": "https://livekit.opytex.org"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"m.server": "synapse.poneyworld.net:443"
|
||||
}
|
||||
29
stacks/matrix.poneyworld.net/secrets.env.yaml
Normal file
29
stacks/matrix.poneyworld.net/secrets.env.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
POSTGRES_PASSWORD: ENC[AES256_GCM,data:vi0nbg1K7psZLvH+ELKJ3uujukcpKL+QYUMHwQhZwED8ZViUZWgMANN8SFjwNJP8RHpgQEQbFjzx2vHuJ3GL1Q==,iv:4OtypnPBzZj6BEVD63ijyD8rufcXAgXO23Hd+KUxXDg=,tag:ctcDhm/+WDhOnBrsO7Fh5A==,type:str]
|
||||
LIVEKIT_KEY: ENC[AES256_GCM,data:d27blhJhDOWnKaMhZOXHBg5N7Xk=,iv:mLjwF8teEeEeJV/DORM5+teNq+bkiVxNmJ797Wnk/Qw=,tag:GjMPv+oyaFFVgf0k2u6hAA==,type:str]
|
||||
LIVEKIT_SECRET: ENC[AES256_GCM,data:mQkMHwaIPBn88Xjayky7avqIq1zovJk8xH9+krLe8xiWZmNTHxwkE9LKxRs/aU3rDeUH/y9OjuDy3QDTq/IpxA==,iv:vH4igaayRzDsKjZJVw4VtWsAcyubTgesZcveLrtxwSM=,tag:kTDK29xCmPIFVGhMkr2aVQ==,type:str]
|
||||
TURN_STATIC_AUTH_SECRET: ENC[AES256_GCM,data:NmgXDsprdI9jP2A4TjHVOv1A2+mnX07zVxYzOrOzU07NiaZIHMYigew/4pMMl2Xz4Rn1K4kFAkXqN/Qr/V+5Rw==,iv:DT5I+u4f8dG2CipxY7f71sGkmmJqre6A9WQPnbxrLxw=,tag:eJ959nOZngMCxvzsmqMOuA==,type:str]
|
||||
TURN_CLI_PASSWORD: ENC[AES256_GCM,data:P1kj2dxT+BBHOWE3zqw82q/w8jrxIjZ1KCQbB05yWW2WjKBBvi1bP041E6sx1Xo7FOS035IX4dUcNo5wpS8Uzg==,iv:VDuQP8EXfc664cjMJJ7NOpvQhkKZF6BKKrTxfBRwjeQ=,tag:pblrz5eboJlz7dyjLqRv3w==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIL3JiNjB6Zkt0MWREWlp6
|
||||
RDYvQjRzSXJaWWJEWkpROU56YzhVTE9jYzBNCmE4dEt3cDNncWhQYlFqRFlVVHVL
|
||||
cGJ0eVo3WEFSVUtNcTJKbENtN1dBeE0KLS0tIDNzVW1hUlpQYmFEMUxyb3hWSUNl
|
||||
TTN6YjFabjZ6QTVtZ3Nwc01WbEs1ckkKD/uwrElZd6/xNbyO/RdPegbDDaS0Kler
|
||||
0CJwZRBumyLirDMVDOMTB8uJqqwCQLppGL8yLsZFMUaNdR3GJpLmFw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age18m2zl3pwgw97djshzcf4wmhrqxksl4erqse6edkpxpkg5rcz0ujs8u4c7s
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOYjYwNTE0RVdQMm5wOHpv
|
||||
RnVrOXRWTUQ0MjRuQlI3cU5hMWNLaGdQb21zCmxzS3lzRjgybWxVQ2xGTStKbVRx
|
||||
V1RFREY1TVJzVTl2Rk5ZQ0QrY3U3ZVUKLS0tIGMvUUZpd01ROWhuRTh6RWdNS0V5
|
||||
K0Zwd0NMWGJKUnJyaEZDMlM1SENzQ28K4tbrrogUL9hLBM49FIT6cQLxyG5/dqj8
|
||||
fifz17xNS4S2YRCOVNal+AI4JXM9yw9hcoB4tt3ileEYcVYl9hzcHg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1fh70nny5hzz8a8g9077kgfca0lhlh3wsjf4fjrv4klx5ucdtpdmsazuqdj
|
||||
lastmodified: "2026-08-19T16:33:04Z"
|
||||
mac: ENC[AES256_GCM,data:Qh5v9F/F24goNXWwinfZskdtRGo5EY1eYwmKoBHb33wdrfQ+wTPjS1XaBnzkxOB9iqk5yna9GTwdqjaZSG/g4KgJRHUQpo+EQieuJn6LDK9YXSCyUGVwQd1ZeHwYypzJLb6JQ+qFwnYnZqOcFGIBGG2E8LFxU7+dFMdRGqn05eo=,iv:nfgJrbMvBQIUjGZoJHNjtv+Uy8fUfjQhxZ9zKcZShnI=,tag:OYAygJJRUkU0QHV7Y6lE7Q==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
6
stacks/paperless.opytex.org/.gitignore
vendored
Normal file
6
stacks/paperless.opytex.org/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# `consume` et `export` sont des bind mounts de DONNÉES, pas de la
|
||||
# configuration : les documents déposés pour ingestion et les exports générés
|
||||
# tombent dans le dépôt puisque les montages sont relatifs à ce dossier.
|
||||
# Sans cette règle, un `git add -A` sur chioggia les committerait.
|
||||
/consume/
|
||||
/export/
|
||||
141
stacks/paperless.opytex.org/README.md
Normal file
141
stacks/paperless.opytex.org/README.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Paperless-ngx — paperless.opytex.org
|
||||
|
||||
Gestion documentaire (OCR + indexation). Cinq conteneurs : `webserver`
|
||||
(l'application), `db` (PostgreSQL), `broker` (Valkey, file de tâches),
|
||||
`gotenberg` et `tika` (conversion des documents bureautiques et des `.eml`).
|
||||
|
||||
Authentification : SSO Authentik, plus un compte admin local (`waha-local`)
|
||||
qui sert de porte de secours si le SSO tombe.
|
||||
|
||||
## Déploiement
|
||||
|
||||
Depuis ce dossier, sur chioggia :
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml 'docker compose up -d'
|
||||
```
|
||||
|
||||
Le nom du dossier (`paperless.opytex.org`) donne le nom de projet Compose
|
||||
`paperlessopytexorg`, dont dérivent le volume `paperlessopytexorg_valkeydata`
|
||||
et le nom du dump de base. **Renommer ce dossier ferait repartir la stack sur
|
||||
des volumes vides.**
|
||||
|
||||
Après une rotation de secret, `docker compose up -d` répond « up to date » et
|
||||
garde l'ancienne valeur :
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml 'docker compose up -d --force-recreate'
|
||||
```
|
||||
|
||||
Les secrets sont déclarés `${VAR:?}` dans le compose : **toute** commande
|
||||
`docker compose` échoue sans l'enveloppe `sops exec-env`, y compris `stop`,
|
||||
`exec`, `logs` et `ps`. Pour enchaîner plusieurs commandes, une seule
|
||||
enveloppe suffit :
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml '
|
||||
docker compose stop webserver &&
|
||||
docker compose exec -T db psql -U paperless -d paperless -c "..." &&
|
||||
docker compose up -d
|
||||
'
|
||||
```
|
||||
|
||||
## Secrets
|
||||
|
||||
Quatre valeurs dans `secrets.env.yaml`, injectées par `sops exec-env` :
|
||||
`PAPERLESS_SECRET_KEY`, `PAPERLESS_DBPASS`, `PAPERLESS_ADMIN_PASSWORD` et
|
||||
`SSO_CLIENT_SECRET`. La clé age de `waha` permet de les éditer directement sur
|
||||
chioggia, sans jamais écrire de fichier en clair :
|
||||
|
||||
```bash
|
||||
sops edit secrets.env.yaml
|
||||
```
|
||||
|
||||
`PAPERLESS_DBPASS` ne se change pas seul : PostgreSQL ignore
|
||||
`POSTGRES_PASSWORD` sur un `PGDATA` déjà initialisé, une nouvelle valeur ne
|
||||
ferait donc que casser la connexion de l'application, en boucle de
|
||||
redémarrage. Il faut la changer aussi dans la base (`ALTER USER`).
|
||||
|
||||
Le `client_id` SSO n'est pas un secret : il est en clair dans
|
||||
`docker-compose.yml`, avec le reste du bloc `PAPERLESS_SOCIALACCOUNT_PROVIDERS`.
|
||||
|
||||
## Le même identifiant sous deux noms
|
||||
|
||||
La base est nommée par deux jeux de variables, dans `env_file` :
|
||||
|
||||
- `PAPERLESS_DBNAME` / `PAPERLESS_DBUSER` : lus par `webserver` pour se
|
||||
connecter.
|
||||
- `POSTGRES_DB` / `POSTGRES_USER` : lus par `db`, et par le script de
|
||||
sauvegarde qui fait `docker exec printenv POSTGRES_DB / POSTGRES_USER /
|
||||
POSTGRES_PASSWORD` sur le conteneur `db`. Le script retombe silencieusement
|
||||
sur `postgres` si la variable manque : le dump réussirait alors sur la
|
||||
mauvaise base.
|
||||
|
||||
Les deux jeux doivent porter **la même valeur**. `POSTGRES_PASSWORD` fait
|
||||
exception : il n'est pas dans `env_file`, il vient de SOPS via
|
||||
`PAPERLESS_DBPASS`, dans le bloc `environment:` du service `db`.
|
||||
|
||||
## `consume` et `export`
|
||||
|
||||
| Volume | Rôle |
|
||||
|---|---|
|
||||
| `consume` | tout fichier déposé ici est ingéré puis supprimé |
|
||||
| `export` | destination de `document_exporter` |
|
||||
|
||||
Ce sont des volumes Docker et non des dossiers de ce dépôt : les données
|
||||
restent hors de l'arbre versionné, et un déplacement du dépôt ne les emmène
|
||||
pas. Ils naissent avec l'UID/GID du chemin correspondant dans l'image (1000),
|
||||
donc sans `chown` à faire.
|
||||
|
||||
Déposer un fichier à ingérer passe par l'UI, ou :
|
||||
|
||||
```bash
|
||||
docker cp fichier.pdf paperlessopytexorg-webserver-1:/usr/src/paperless/consume/
|
||||
```
|
||||
|
||||
Générer une archive lisible sans Paperless :
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml \
|
||||
'docker compose exec webserver document_exporter ../export'
|
||||
```
|
||||
|
||||
## Volumes
|
||||
|
||||
| Volume | Contenu | Sauvegarde |
|
||||
|---|---|---|
|
||||
| `paperless-data` | index de recherche, classifieur, base des tâches | label `backup.enable=true` + `MANUAL_VOLUMES` |
|
||||
| `paperless-media` | **les documents originaux et leurs archives PDF** | label `backup.enable=true` + `MANUAL_VOLUMES` |
|
||||
| `paperless-pg-data` | base PostgreSQL | dump SQL (`backup.type=postgres` sur le conteneur `db`) |
|
||||
| `paperlessopytexorg_valkeydata` | file de tâches Celery (Valkey) | non sauvegardé, volontairement — reconstruit |
|
||||
| `paperlessopytexorg_export` | archive `document_exporter`, lisible sans Paperless | label `backup.enable=true` sur le volume |
|
||||
| `paperlessopytexorg_consume` | sas d'ingestion | non sauvegardé, volontairement — transitoire |
|
||||
|
||||
Les trois premiers portent un `name:` explicite, sans préfixe de projet.
|
||||
|
||||
Les labels de volume ne s'appliquent qu'à la création : en ajouter un — ou en
|
||||
retirer un — ne changerait rien au volume réel, mais ferait diverger sa
|
||||
déclaration, et Compose proposerait alors de le **recréer**. Répondre « y » à
|
||||
cette question détruirait les documents.
|
||||
|
||||
`paperless-data` et `paperless-media` figurent en plus dans `MANUAL_VOLUMES`
|
||||
(`backup.conf`), ce qui les fait sauvegarder deux fois — sans conséquence,
|
||||
restic déduplique. `paperless-pg-data` n'a délibérément aucun label :
|
||||
sauvegarder les fichiers d'un PostgreSQL en cours d'écriture ne donne pas une
|
||||
base restaurable, c'est le dump SQL qui le couvre.
|
||||
|
||||
## Version des images
|
||||
|
||||
Paperless-ngx applique des migrations de schéma **irréversibles** à chaque
|
||||
montée de version : une mise à jour se fait seule, après sauvegarde vérifiée,
|
||||
et ne se revient pas en repassant l'image à la version précédente.
|
||||
|
||||
Toutes les images sont épinglées au patch, y compris `apache/tika` que la
|
||||
documentation amont laisse en `latest` : un tag flottant rend la stack non
|
||||
reproductible et fait franchir des versions au prochain `pull`, sans que rien
|
||||
ne l'annonce.
|
||||
|
||||
Références :
|
||||
|
||||
- https://docs.paperless-ngx.com/configuration/
|
||||
- https://docs.goauthentik.io/integrations/services/paperless-ngx/
|
||||
126
stacks/paperless.opytex.org/docker-compose.yml
Normal file
126
stacks/paperless.opytex.org/docker-compose.yml
Normal file
@@ -0,0 +1,126 @@
|
||||
services:
|
||||
broker:
|
||||
image: docker.io/valkey/valkey:9.1.1-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- valkeydata:/data
|
||||
networks:
|
||||
- default
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:16.15
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- env_file
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${PAPERLESS_DBPASS:?}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
networks:
|
||||
- default
|
||||
labels:
|
||||
- backup.enable=true
|
||||
- backup.type=postgres
|
||||
|
||||
webserver:
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:3.0.5
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- db
|
||||
- broker
|
||||
- gotenberg
|
||||
- tika
|
||||
env_file:
|
||||
- env_file
|
||||
environment:
|
||||
PAPERLESS_DBPASS: ${PAPERLESS_DBPASS:?}
|
||||
PAPERLESS_SECRET_KEY: ${PAPERLESS_SECRET_KEY:?}
|
||||
PAPERLESS_ADMIN_PASSWORD: ${PAPERLESS_ADMIN_PASSWORD:?}
|
||||
# Le client_id n'est pas un secret ; seul `secret` vient de SOPS.
|
||||
# Ce bloc reste dans `environment:` et non dans env_file : lui seul
|
||||
# subit l'interpolation ${...} de Compose.
|
||||
PAPERLESS_SOCIALACCOUNT_PROVIDERS: >
|
||||
{
|
||||
"openid_connect": {
|
||||
"APPS": [
|
||||
{
|
||||
"provider_id": "authentik",
|
||||
"name": "Authentik",
|
||||
"client_id": "wu1TWyQ5sIADMhyy04rcOWo95Ciu8g0yKizKF4QQ",
|
||||
"secret": "${SSO_CLIENT_SECRET:?}",
|
||||
"settings": {
|
||||
"server_url": "https://sso.opytex.org/application/o/paperless/.well-known/openid-configuration"
|
||||
}
|
||||
}
|
||||
],
|
||||
"OAUTH_PKCE_ENABLED": "True"
|
||||
}
|
||||
}
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
- export:/usr/src/paperless/export
|
||||
- consume:/usr/src/paperless/consume
|
||||
networks:
|
||||
- default
|
||||
- traefik-proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-proxy
|
||||
- traefik.http.routers.paperlessngx.entrypoints=web-secure
|
||||
- traefik.http.routers.paperlessngx.rule=Host(`paperless.opytex.org`)
|
||||
- traefik.http.routers.paperlessngx.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.paperlessngx.middlewares=crowdsec@file
|
||||
- traefik.http.services.paperlessngx.loadbalancer.server.port=8000
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:8.34.0
|
||||
restart: unless-stopped
|
||||
# La route chromium sert à convertir les .eml : ni javascript ni contenu
|
||||
# distant (pixels de tracking).
|
||||
command:
|
||||
- "gotenberg"
|
||||
- "--chromium-disable-javascript=true"
|
||||
- "--chromium-allow-list=file:///tmp/.*"
|
||||
networks:
|
||||
- default
|
||||
|
||||
tika:
|
||||
image: docker.io/apache/tika:3.3.1.0
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
|
||||
volumes:
|
||||
# Déclarations reprises telles quelles de l'ancien compose : les volumes
|
||||
# existent déjà sur chioggia, et un label de volume ne s'applique qu'à la
|
||||
# création. Ajouter ou retirer un label ferait diverger la déclaration du
|
||||
# volume réel, et Compose proposerait de le RECRÉER.
|
||||
#
|
||||
# `name:` fige les noms hérités, sans le préfixe de projet.
|
||||
data:
|
||||
name: paperless-data
|
||||
labels:
|
||||
backup.enable: true
|
||||
media:
|
||||
name: paperless-media
|
||||
labels:
|
||||
backup.enable: true
|
||||
pgdata:
|
||||
name: paperless-pg-data
|
||||
# Volume neuf : Valkey 9 (fork de Redis 7.2) ne sait pas lire un `dump.rdb`
|
||||
# écrit par Redis 7.4. Le contenu n'étant que la file Celery, on repart de
|
||||
# zéro plutôt que de convertir. L'ancien `paperlessopytexorg_redisdata`
|
||||
# subsiste, inutilisé : c'est le chemin de retour vers `redis:7`.
|
||||
valkeydata:
|
||||
# Volumes neufs : le label s'applique donc, contrairement aux quatre
|
||||
# au-dessus. `consume` n'en porte pas — un fichier n'y séjourne que le temps
|
||||
# d'être ingéré, après quoi il est dans `media`.
|
||||
export:
|
||||
labels:
|
||||
backup.enable: true
|
||||
consume:
|
||||
|
||||
networks:
|
||||
traefik-proxy:
|
||||
external: true
|
||||
62
stacks/paperless.opytex.org/env_file
Normal file
62
stacks/paperless.opytex.org/env_file
Normal file
@@ -0,0 +1,62 @@
|
||||
# =============================================================================
|
||||
# PAPERLESS-NGX — configuration NON sensible
|
||||
# =============================================================================
|
||||
# PAPERLESS_SECRET_KEY, PAPERLESS_DBPASS, PAPERLESS_ADMIN_PASSWORD et
|
||||
# SSO_CLIENT_SECRET sont dans secrets.env.yaml, injectés par `sops exec-env`.
|
||||
#
|
||||
# Ce fichier ne contient QUE ce que les conteneurs consomment. Le domaine, la
|
||||
# version d'image et le bloc SSO restent en dur dans docker-compose.yml :
|
||||
# `env_file:` n'alimente pas l'interpolation ${...}, et une version en dur est
|
||||
# détectée par Renovate.
|
||||
# =============================================================================
|
||||
|
||||
# --- UID/GID du process paperless dans le conteneur ---
|
||||
# Doit correspondre au propriétaire du dossier `consume` sur l'hôte.
|
||||
USERMAP_UID=1000
|
||||
USERMAP_GID=1000
|
||||
|
||||
# --- Domaine ---
|
||||
PAPERLESS_URL=https://paperless.opytex.org
|
||||
|
||||
# --- Divers ---
|
||||
PAPERLESS_TIME_ZONE=Europe/Paris
|
||||
|
||||
# --- OCR ---
|
||||
# LANGUAGES : paquets tesseract installés au démarrage.
|
||||
# LANGUAGE : langue utilisée pour l'OCR.
|
||||
PAPERLESS_OCR_LANGUAGES=eng fra
|
||||
PAPERLESS_OCR_LANGUAGE=fra+eng
|
||||
|
||||
# --- Services internes ---
|
||||
PAPERLESS_REDIS=redis://broker:6379
|
||||
PAPERLESS_DBHOST=db
|
||||
PAPERLESS_TIKA_ENABLED=1
|
||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT=http://gotenberg:3000
|
||||
PAPERLESS_TIKA_ENDPOINT=http://tika:9998
|
||||
|
||||
# --- Base de données ---
|
||||
# POSTGRES_* est lu par le conteneur `db` (et par le script de sauvegarde, qui
|
||||
# fait `docker exec printenv POSTGRES_DB/USER/PASSWORD` pour le dump).
|
||||
# PAPERLESS_DB* est lu par le conteneur `webserver`. Les deux doivent
|
||||
# concorder.
|
||||
POSTGRES_DB=paperless
|
||||
POSTGRES_USER=paperless
|
||||
PAPERLESS_DBNAME=paperless
|
||||
PAPERLESS_DBUSER=paperless
|
||||
|
||||
# --- Compte administrateur local ---
|
||||
# Porte de secours si le SSO tombe. Le mot de passe est dans secrets.env.yaml.
|
||||
PAPERLESS_ADMIN_USER=waha-local
|
||||
|
||||
# --- SSO OpenID Connect (Authentik) ---
|
||||
# Le provider lui-même est déclaré dans PAPERLESS_SOCIALACCOUNT_PROVIDERS,
|
||||
# côté docker-compose.yml.
|
||||
PAPERLESS_APPS=allauth.socialaccount.providers.openid_connect
|
||||
|
||||
# --- Requis à partir de la v3 ---
|
||||
# Le moteur n'est plus déduit de PAPERLESS_DBHOST. La valeur attendue est
|
||||
# `postgresql` et non `postgres`.
|
||||
PAPERLESS_DBENGINE=postgresql
|
||||
# La v3 accepte les doublons par défaut ; ceci restaure le refus, qui était le
|
||||
# comportement de la v2.
|
||||
PAPERLESS_CONSUMER_DELETE_DUPLICATES=true
|
||||
28
stacks/paperless.opytex.org/secrets.env.yaml
Normal file
28
stacks/paperless.opytex.org/secrets.env.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
PAPERLESS_SECRET_KEY: ENC[AES256_GCM,data:tE2X6Pt/bSMpbgVo6IGQMjZ0p4AsQEXoQft8l7PNgBw1EWcjz0mSJ5+tjGgCcSnzRAo=,iv:LvVv5PCgy6ZnitpG2y5JG95V10/Ky0hgN9JyND5w+oM=,tag:bGXbX3+IP/vcj5SluPryqw==,type:str]
|
||||
PAPERLESS_ADMIN_PASSWORD: ENC[AES256_GCM,data:jhmtzvaumj8kepDLGaMFUDVLZug=,iv:wdhUgllGyPadWGhOqt4nTprBO+uJM1ksNVgYRAfmt8Q=,tag:2n/aQKW8eJgNyozTbRkd6A==,type:str]
|
||||
PAPERLESS_DBPASS: ENC[AES256_GCM,data:9lvz6bH9toUY,iv:QV6fb3oVNiGiMTrmegzW11eP1GVlEYIfxn/Vexcmqu4=,tag:v+ElzCZuTuc6LPRclPvBww==,type:str]
|
||||
SSO_CLIENT_SECRET: ENC[AES256_GCM,data:YQEDOt9SlbJJ+ex5s4Ix4G/xtCZIT96UlE4u9FKngYfE+dlpKjoqvMq4iCGNxF85ybEpnF69UmmTz1apBIPMkCJGphwNESi6ZDIaHHeUli5ZfOaikU6EJISmM5C83q/z3P4uTKGiYzHOrwqoR7q/I7AI010nYaaLLIFtoaS6CjY=,iv:TOQjuZGR5QLmGWtkbBybatTDodwE2r7qXUnAKK4j+ys=,tag:RZEayii5Za5UEfoV+uFXpQ==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJQ3N5YS9oV2drUHRQTEhj
|
||||
ZXcyeXIyRkFETlBiZWY2RlZtSVZTL3ZxS1djCmFEanRPUWwzSjAyNU55bGJBanZx
|
||||
SnFYUmZYV2lMbnhEeE1Ob0JaWlE1ZDAKLS0tIFZOSGVtYUphR0cxdnd4VnpMd3pE
|
||||
eUxTb20yZnIxcWdsU3ZJdi9hZUdYd1UKVkUwEN2sjeuz3uDy/qXqfZR0XboYiNlY
|
||||
mTxiZ+Kn9lpCUgWJql4DJbYFffUMOtJIPjbfOudZRNZe9N/ez0nZHA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age18m2zl3pwgw97djshzcf4wmhrqxksl4erqse6edkpxpkg5rcz0ujs8u4c7s
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPTDRkK25xOTFqQ2pjcHFk
|
||||
WnB5R0VPY1dKdjU3Q0xJaTI5eFVFR0VzdGtVCmMxRHdUMTdlWE1aNVlZVmdxNkNu
|
||||
Rit0YjBSbTVSNG9lN29CMThldzU0MEUKLS0tIFpiYzFRZUZZQlRwV1pmOWVYUlRY
|
||||
cWYyUnNQU1dqMndoK09DQmNUR1d6Y2MKLGPkFeMO4XBhnNY63D2C5dO+X6nlXhQ6
|
||||
AP2xgImvNy3DSxDfjb8o6dfhwZpMHLLyK6bUqb9afLnTt67E9WtmvQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1fh70nny5hzz8a8g9077kgfca0lhlh3wsjf4fjrv4klx5ucdtpdmsazuqdj
|
||||
lastmodified: "2026-08-17T14:20:44Z"
|
||||
mac: ENC[AES256_GCM,data:s04pAb0JwEdjCgY6vA6ZtBWulrESprXtAuyUzzfzPnVWXa/fv7e84ur2jwI+iKNh/T1SEYeCl5MQ8bV6qZiGIWWDXo4YVlHGxUl+peLOek9Ej2StYkNh7ntm4mOaYug60z7/nIAUdjIc81xo9HT1TLnBnrGZFH8uZ2/1aizug3U=,iv:DhvmLJLbRZC2MoLTEzwQaRvsv5gfixfnOpN9bTQlmOM=,tag:wEODwURmDKJNUhmJkh4pyQ==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
52
stacks/rss.opytex.org/README.md
Normal file
52
stacks/rss.opytex.org/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# FreshRSS — rss.opytex.org
|
||||
|
||||
Agrégateur RSS, PostgreSQL dédié, authentification SSO via Authentik.
|
||||
|
||||
## Déploiement
|
||||
|
||||
Depuis ce dossier, sur chioggia :
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml 'docker compose up -d'
|
||||
```
|
||||
|
||||
Le nom du dossier (`rss.opytex.org`) donne le nom de projet Compose
|
||||
`rssopytexorg`, dont dérivent les noms de volumes
|
||||
(`rssopytexorg_freshrss_data`, …) et le nom du dump de base
|
||||
(`rssopytexorg.sql.gz`). **Renommer ce dossier ferait repartir la stack sur
|
||||
des volumes vides.**
|
||||
|
||||
Après une rotation de secret, `docker compose up -d` répond « up to date » et
|
||||
garde l'ancienne valeur :
|
||||
|
||||
```bash
|
||||
sops exec-env secrets.env.yaml 'docker compose up -d --force-recreate'
|
||||
```
|
||||
|
||||
## Volumes
|
||||
|
||||
| Volume | Contenu | Sauvegarde |
|
||||
| ---------------------------------- | -------------------------------- | -------------------------------------------------- |
|
||||
| `rssopytexorg_pgsql_data` | base PostgreSQL | dump SQL (`backup.type=postgres` sur le conteneur) |
|
||||
| `rssopytexorg_freshrss_data` | config, comptes, articles locaux | label `backup.enable=true` sur le volume |
|
||||
| `rssopytexorg_freshrss_extensions` | extensions installées | label `backup.enable=true` sur le volume |
|
||||
|
||||
Les deux volumes FreshRSS portent `backup.enable=true` **depuis leur
|
||||
création** : le label est donc conservé dans le compose. Le retirer, comme
|
||||
l'ajouter à un volume qui ne l'a pas, ferait diverger la déclaration du volume
|
||||
réel — Compose proposerait alors de le recréer, donc de vider FreshRSS.
|
||||
|
||||
Ils figurent en plus dans `MANUAL_VOLUMES` (`backup.conf`), ce qui les fait
|
||||
sauvegarder deux fois : le script empile les chemins de `MANUAL_VOLUMES` puis
|
||||
ceux des volumes labellisés, sans dédoublonner. Redondance héritée de l'ancien
|
||||
dépôt, sans conséquence (restic déduplique) — à nettoyer quand `backup.conf`
|
||||
sera migré dans `ops/backup/`.
|
||||
|
||||
`pgsql_data` n'a délibérément aucun label : sauvegarder les fichiers d'un
|
||||
PostgreSQL en cours d'écriture ne donne pas une base restaurable, c'est le dump
|
||||
SQL qui le couvre.
|
||||
|
||||
Références :
|
||||
|
||||
- https://freshrss.github.io/FreshRSS/en/admins/16_OpenID-Connect-Authentik.html
|
||||
- https://docs.goauthentik.io/integrations/services/freshrss/
|
||||
53
stacks/rss.opytex.org/docker-compose.yml
Normal file
53
stacks/rss.opytex.org/docker-compose.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:16.15
|
||||
container_name: freshrss-db
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- env_file
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
|
||||
volumes:
|
||||
- pgsql_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- default
|
||||
labels:
|
||||
- backup.enable=true
|
||||
- backup.type=postgres
|
||||
|
||||
freshrss:
|
||||
image: freshrss/freshrss:1.29.1
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- postgresql
|
||||
env_file:
|
||||
- env_file
|
||||
environment:
|
||||
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:?}
|
||||
volumes:
|
||||
- freshrss_data:/var/www/FreshRSS/data
|
||||
- freshrss_extensions:/var/www/FreshRSS/extensions
|
||||
networks:
|
||||
- default
|
||||
- traefik-proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-proxy
|
||||
- traefik.http.routers.freshrss.entrypoints=web-secure
|
||||
- traefik.http.routers.freshrss.rule=Host(`rss.opytex.org`)
|
||||
- traefik.http.routers.freshrss.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.freshrss.middlewares=crowdsec@file
|
||||
- traefik.http.services.freshrss.loadbalancer.server.port=80
|
||||
|
||||
volumes:
|
||||
pgsql_data:
|
||||
freshrss_data:
|
||||
labels:
|
||||
backup.enable: true
|
||||
freshrss_extensions:
|
||||
labels:
|
||||
backup.enable: true
|
||||
|
||||
networks:
|
||||
traefik-proxy:
|
||||
external: true
|
||||
26
stacks/rss.opytex.org/env_file
Normal file
26
stacks/rss.opytex.org/env_file
Normal file
@@ -0,0 +1,26 @@
|
||||
# =============================================================================
|
||||
# FRESHRSS — configuration NON sensible
|
||||
# =============================================================================
|
||||
# POSTGRES_PASSWORD et OIDC_CLIENT_SECRET sont dans secrets.env.yaml et
|
||||
# injectés par `sops exec-env`.
|
||||
#
|
||||
# Ce fichier ne contient QUE ce que les conteneurs consomment. Le domaine et
|
||||
# la version d'image sont en dur dans docker-compose.yml : `env_file:`
|
||||
# n'alimente pas l'interpolation ${...}, et une version en dur est détectée
|
||||
# par Renovate.
|
||||
# =============================================================================
|
||||
|
||||
# --- Base de données ---
|
||||
POSTGRES_USER=freshrss
|
||||
POSTGRES_DB=freshrss
|
||||
|
||||
# --- Conteneur FreshRSS ---
|
||||
CRON_MIN=*/20
|
||||
TZ=Europe/Paris
|
||||
|
||||
# --- SSO OpenID Connect (Authentik) ---
|
||||
OIDC_ENABLED=1
|
||||
OIDC_PROVIDER_METADATA_URL=https://sso.opytex.org/application/o/freshrss/.well-known/openid-configuration
|
||||
OIDC_CLIENT_ID=3YF3MIapyVz45EQ8BcFGwk2q47BTstmPMhRwwPlZ
|
||||
OIDC_X_FORWARDED_HEADERS="X-Forwarded-Port X-Forwarded-Proto X-Forwarded-Host"
|
||||
OIDC_SCOPES="openid email profile"
|
||||
26
stacks/rss.opytex.org/secrets.env.yaml
Normal file
26
stacks/rss.opytex.org/secrets.env.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
POSTGRES_PASSWORD: ENC[AES256_GCM,data:2QAKvkuKOxME53FF3YGugKZSxVjfgF4NEXRMFNmSP6Z9ICkUHIQvFlxcFCKIJ1L8,iv:shO6GH4mlqPpEXO9Q/0LgUb7l/zDOghDbTxMzlkJBjg=,tag:vlBr1hE4GimoobZ8+ejgrg==,type:str]
|
||||
OIDC_CLIENT_SECRET: ENC[AES256_GCM,data:5dlCxqpNV7KVrMAP4RrSynI3Vd0SdlbaUxZXO4d7Tc315iMqLP3pyUWKc7qHdgQeWeOnX1nq3S0IcFwdifmGi87InkBkxorvKCIC6IK6yxm46OOVuYlX39KPVMpIPgX857sPk/7wRp9CHTcvyTIE0s1tspZZX3qe693e+oso0Oo=,iv:GTSQ0G3la61BeNDrMaHeYMZ7Xk+iGuQySyNCKeao1vo=,tag:t6YGToLryI7CvASr8crwiA==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNUS9xaUMxV2EreEVwdmFV
|
||||
c1kzbkNOOCt2WisvQjhvU0wxb0hSUkRkMUVRCnFxUmNXNUhoemRKWjlpN2E0Z0kz
|
||||
cWorU3ZINm5ISEhMWWRsVVFJRUFqZ1EKLS0tIGVMZUloa2RxK0daWHVFNW1CSzZZ
|
||||
NVhNZUxjcmZTYTk5UjNZUmFrRnlXeW8KQVe3zJMofheTzg6vDtKSEYefRFz9ND9v
|
||||
CPTWmjv0tGAsWX6sdnKeBniw29A9QRKlAKubvES7hl4cf2vEtruYzQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age18m2zl3pwgw97djshzcf4wmhrqxksl4erqse6edkpxpkg5rcz0ujs8u4c7s
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsZEVuejgwMXhCV1BCaHlp
|
||||
RG5TZklLa0xQYWVTTU40VVhOSEVsb2xHZXlVClJiYXhtZ2ZCRklRTGRGSVBtaUFP
|
||||
VXFjME9WZHhyRXJDazR4VnV2R0VXaWMKLS0tIE9LcmZjMFFhRWJqbU52S1BHSldx
|
||||
Wk8wcis2bzgwSWpFVTUwOC9mT3UzVjQK4X6/iYoec/k1r/yLIQoT0XhfYryJ75KN
|
||||
X7aDRlHniIPPPUg9tdIjGIUKpRBidOBw7s1PYImxOMTqjs0CUoIRzw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1fh70nny5hzz8a8g9077kgfca0lhlh3wsjf4fjrv4klx5ucdtpdmsazuqdj
|
||||
lastmodified: "2026-08-17T13:14:12Z"
|
||||
mac: ENC[AES256_GCM,data:FcSRI1QAG7k/yXrx42HOwhbtTLKGl72z7p999qoVZxVkXVNaCpdU4KE3upK27ADJ/8LGCNT1/pbsHRyrxcOZXJrAc2/zC8ZEXWdJdKvV4R/pr2dH/ESZKi4wYGuvzTFbjP0HoAuSRQchSRaogWCGbMpEk2vtE22CkdOZD92T2n8=,iv:k7e5MpMnSp6bcks0cjI76NP75yjwW3KEyZxiw0k1FVs=,tag:0xDH+8/PV+UzLMLYcrTz4w==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
42
stacks/traefik/docker-compose.yml
Normal file
42
stacks/traefik/docker-compose.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.6
|
||||
container_name: traefik2
|
||||
restart: always
|
||||
# Pas de `command:` : Traefik lit /etc/traefik/traefik.toml par défaut.
|
||||
# Un `--log.level` ici écraserait silencieusement celui du toml.
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./traefik.toml:/etc/traefik/traefik.toml:ro
|
||||
- ./dynamic:/etc/traefik/dynamic:ro
|
||||
# Doit exister comme FICHIER avant le premier `up`, sinon Docker crée un
|
||||
# répertoire à sa place et Traefik redemande tous les certificats.
|
||||
- ./acme.json:/acme.json
|
||||
- traefik_log:/var/log/traefik/
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
secrets:
|
||||
- crowdsec_lapi_key
|
||||
networks:
|
||||
- traefik-proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-proxy
|
||||
- traefik.http.routers.traefik_secure.entrypoints=web-secure
|
||||
- traefik.http.routers.traefik_secure.rule=Host(`chioggia.opytex.org`)
|
||||
- traefik.http.routers.traefik_secure.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.traefik_secure.service=api@internal
|
||||
- traefik.http.routers.traefik_secure.middlewares=authentik@file
|
||||
|
||||
secrets:
|
||||
crowdsec_lapi_key:
|
||||
environment: CROWDSEC_LAPI_KEY
|
||||
|
||||
volumes:
|
||||
traefik_log:
|
||||
external: true
|
||||
|
||||
networks:
|
||||
traefik-proxy:
|
||||
external: true
|
||||
18
stacks/traefik/dynamic/chioggia.opytex.yml
Normal file
18
stacks/traefik/dynamic/chioggia.opytex.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
http:
|
||||
middlewares:
|
||||
authentik:
|
||||
forwardAuth:
|
||||
address: http://authentik:9000/outpost.goauthentik.io/auth/traefik
|
||||
trustForwardHeader: true
|
||||
authResponseHeaders:
|
||||
- X-authentik-username
|
||||
- X-authentik-groups
|
||||
- X-authentik-email
|
||||
- X-authentik-name
|
||||
- X-authentik-uid
|
||||
- X-authentik-jwt
|
||||
- X-authentik-meta-jwks
|
||||
- X-authentik-meta-outpost
|
||||
- X-authentik-meta-provider
|
||||
- X-authentik-meta-app
|
||||
- X-authentik-meta-version
|
||||
18
stacks/traefik/dynamic/crowdsec.yml
Normal file
18
stacks/traefik/dynamic/crowdsec.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
http:
|
||||
middlewares:
|
||||
crowdsec:
|
||||
plugin:
|
||||
bouncer:
|
||||
enabled: true
|
||||
crowdsecMode: stream
|
||||
crowdsecLapiScheme: http
|
||||
crowdsecLapiHost: crowdsec:8080
|
||||
# La clé elle-même est dans secrets.env.yaml. Le provider fichier de
|
||||
# Traefik ne substitue aucune variable d'environnement : c'est ce
|
||||
# `...File` qui permet de garder ce fichier versionnable en clair.
|
||||
crowdsecLapiKeyFile: /run/secrets/crowdsec_lapi_key
|
||||
logLevel: ERROR
|
||||
# crowdsecAppsecEnabled: true
|
||||
# crowdsecAppsecHost: crowdsec:7422
|
||||
# crowdsecAppsecFailureBlock: false
|
||||
# crowdsecAppsecUnreachableBlock: false
|
||||
28
stacks/traefik/dynamic/opytex.toml
Normal file
28
stacks/traefik/dynamic/opytex.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[http]
|
||||
|
||||
[http.routers]
|
||||
[http.routers.opytexorg]
|
||||
entryPoints = ["web-secure"]
|
||||
service = "opytexorg"
|
||||
middlewares = ["opytexorgstripprefix@file", "opytexorgindex@file", "opytexerror@file"]
|
||||
rule = "Host(`opytex.org`)"
|
||||
[http.routers.opytexorg.tls]
|
||||
certResolver = "letsencrypt"
|
||||
[[http.routers.opytexorg.tls.domains]]
|
||||
main = "opytex.org"
|
||||
|
||||
[http.services]
|
||||
[http.services.opytexorg.loadBalancer]
|
||||
[[http.services.opytexorg.loadBalancer.servers]]
|
||||
url = "http://minio:9000"
|
||||
|
||||
[http.middlewares]
|
||||
[http.middlewares.opytexorgindex.replacePathRegex]
|
||||
regex = "^(.*)/$"
|
||||
replacement = "$1/index.html"
|
||||
[http.middlewares.opytexorgstripprefix.addPrefix]
|
||||
prefix = "/opytex.org"
|
||||
[http.middlewares.opytexerror.errors]
|
||||
status = ["400-499", "500-599"]
|
||||
service = "opytexorg@file"
|
||||
query = "opytex.org/404.html"
|
||||
22
stacks/traefik/dynamic/raw.toml
Normal file
22
stacks/traefik/dynamic/raw.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[http]
|
||||
|
||||
[http.routers]
|
||||
[http.routers.raw]
|
||||
entryPoints = ["web-secure"]
|
||||
service = "raw"
|
||||
middlewares = ["rawstripprefix@file"]
|
||||
rule = "Host(`raw.opytex.org`)"
|
||||
[http.routers.raw.tls]
|
||||
certResolver = "letsencrypt"
|
||||
[[http.routers.raw.tls.domains]]
|
||||
main = "raw.opytex.org"
|
||||
|
||||
[http.services]
|
||||
[http.services.raw.loadBalancer]
|
||||
[[http.services.raw.loadBalancer.servers]]
|
||||
url = "http://minio:9000"
|
||||
|
||||
[http.middlewares]
|
||||
[http.middlewares.rawstripprefix.addPrefix]
|
||||
prefix = "/raw"
|
||||
|
||||
25
stacks/traefik/secrets.env.yaml
Normal file
25
stacks/traefik/secrets.env.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
CROWDSEC_LAPI_KEY: ENC[AES256_GCM,data:p3i4Y2EkE1uygmNy5obsX/5OB0Q6OR1PyQ54PLntmxQOejqSYvMaKwpz7A==,iv:Yl/C/hChLU5Kp4kO0npU+BCnSrsOMR21JPxkWWv+Sx4=,tag:Yh45ejBdOR4aBCMbxfpCxA==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkeDlpb3N3amFuTTcwamFy
|
||||
a0pSTDhTRExLUUxjM1ZIcmU2Rko3enM1dkZNCm9OZHdxcTBxQnB2RzFEbkNOTEJ6
|
||||
WUdTR3NBeXV3KzlnM2N1YkIvbDJJQUUKLS0tIE9ZQzVKbnZES2tRdkhpUjZjQTdP
|
||||
Qm1EdGR6TitNaTFVS1pyTGhGK08wbGMK9ZwsDE2ih9EI87MNqFuA65XFZBAL2akF
|
||||
4ftZM3DDQsvL62DWPYi6t466zg1kH2R2zx/wz9Jil4LKY13vJgz+bQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age18m2zl3pwgw97djshzcf4wmhrqxksl4erqse6edkpxpkg5rcz0ujs8u4c7s
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlUS9sZXVCLzIwcE4xdXdx
|
||||
K2R5eTJnTWs3NHRlbEVsMEFhSnJIdGdpS0ZzCmNmVnVqYnlUS09pK25hcFJySnRn
|
||||
SS81MHpJYksyaFNUeXFEWWFPQXU2R1EKLS0tIHBnZU1OcGZsSnRCM1hIYWlIZ3hK
|
||||
UGZhSjVRRldXOUI2d3lZNlM1WE9lb2cK1ByWPZ8taCHED0jQiUkldAehZ7KQYPJx
|
||||
E20o7wnn45LHx1Km+R91CeNTj7NFIXCpLZrhcXo89knvL4qFNCkT+Q==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1fh70nny5hzz8a8g9077kgfca0lhlh3wsjf4fjrv4klx5ucdtpdmsazuqdj
|
||||
lastmodified: "2026-08-14T04:27:43Z"
|
||||
mac: ENC[AES256_GCM,data:DkOHuAVAZM5f7uDu7BmZtGNWZlOEsU93aWdYw3NzDxcyZji4d1r//OHKtRPyrV02D+FvusU56gjjMkf3B9gKhNjH/vSiQCVbSzDbkbo7sG/YJPFK81IDJs1CAR9zgzTzNj9vRXCZUSvVDkhPh/zKKKkgC8fe5MScLnYZMiVfUt4=,iv:vgI6Hw7Coql1zWvr7jfMMMc538lyG5x9uY2aqr6ZTHc=,tag:EV/fCwtfK+UebeB6390Slg==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
54
stacks/traefik/traefik.toml
Normal file
54
stacks/traefik/traefik.toml
Normal file
@@ -0,0 +1,54 @@
|
||||
[global]
|
||||
checkNewVersion = true
|
||||
sendAnonymousUsage = false
|
||||
|
||||
[log]
|
||||
level = "INFO"
|
||||
filePath = "/var/log/traefik/traefik.log"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":80"
|
||||
[entryPoints.web.http.redirections.entryPoint]
|
||||
to = "web-secure"
|
||||
scheme = "https"
|
||||
|
||||
[entryPoints.web-secure]
|
||||
address = ":443"
|
||||
|
||||
[accessLog]
|
||||
filePath = "/var/log/traefik/access.log"
|
||||
bufferingSize = 100
|
||||
|
||||
[providers]
|
||||
[providers.file]
|
||||
directory = "/etc/traefik/dynamic"
|
||||
|
||||
[providers.docker]
|
||||
endpoint = "unix:///var/run/docker.sock"
|
||||
watch = true
|
||||
exposedbydefault = false
|
||||
|
||||
[api]
|
||||
dashboard = true
|
||||
debug = false
|
||||
|
||||
# Sans ce bloc, `plugin.bouncer` référencé par dynamic/crowdsec.yml n'existe
|
||||
# pas : le middleware crowdsec@file devient invalide et TOUS les routeurs qui
|
||||
# l'utilisent tombent en erreur (404). Traefik ne lit cette configuration qu'au
|
||||
# démarrage — un conteneur déjà lancé survit à sa disparition, la panne
|
||||
# n'apparaît qu'à la recréation suivante. C'est arrivé le 2026-08-13.
|
||||
[experimental]
|
||||
[experimental.plugins.bouncer]
|
||||
moduleName = "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
version = "v1.4.5"
|
||||
|
||||
[certificatesResolvers.letsencrypt.acme]
|
||||
email = "benjamin.bertrand@opytex.org"
|
||||
# Chemin ABSOLU, et monté depuis l'hôte. En relatif, le fichier atterrissait
|
||||
# dans le répertoire de travail du conteneur : perdu à chaque recréation,
|
||||
# avec réémission complète des certificats et plafond Let's Encrypt au bout.
|
||||
storage = "/acme.json"
|
||||
tlschallenge = true
|
||||
[certificatesResolvers.letsencrypt.acme.httpChallenge]
|
||||
entryPoint = "web"
|
||||
Reference in New Issue
Block a user