Compare commits

...

11 Commits

14 changed files with 648 additions and 66 deletions

View File

@@ -285,3 +285,19 @@ resource "ovh_domain_zone_record" "maintenant_aaaa" {
ttl = 3600 ttl = 3600
target = "2001:41d0:2:bbb6:0:0:0:1" 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"
}

View 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/

View 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

View 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

View 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

View 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

View File

@@ -1,6 +1,6 @@
services: services:
maintenant: maintenant:
image: ghcr.io/kolapsis/maintenant:1.3.5 image: ghcr.io/kolapsis/maintenant:1.3.9
container_name: maintenant container_name: maintenant
restart: unless-stopped restart: unless-stopped
read_only: true read_only: true

View File

@@ -77,10 +77,9 @@ Cinq valeurs dans `secrets.env.yaml`, injectées par `sops exec-env` :
| `LIVEKIT_KEY`, `LIVEKIT_SECRET` | `livekit` (via `LIVEKIT_KEYS`) et `lk-jwt` | | `LIVEKIT_KEY`, `LIVEKIT_SECRET` | `livekit` (via `LIVEKIT_KEYS`) et `lk-jwt` |
| `TURN_STATIC_AUTH_SECRET`, `TURN_CLI_PASSWORD` | `coturn` | | `TURN_STATIC_AUTH_SECRET`, `TURN_CLI_PASSWORD` | `coturn` |
`LIVEKIT_KEY` est un identifiant plus qu'un secret, et la convention du dépôt `LIVEKIT_KEY` tient plus de l'identifiant que du secret. Il est chiffré avec
laisserait ce genre de valeur en clair. Il est chiffré quand même : il ne se son secret parce que les deux se tournent toujours ensemble : les séparer sur
tourne jamais seul, toujours en paire avec son secret, et le séparer sur deux deux mécanismes rendrait la rotation facile à ne faire qu'à moitié.
mécanismes rendrait la rotation facile à rater à moitié.
```bash ```bash
sops edit secrets.env.yaml sops edit secrets.env.yaml
@@ -110,14 +109,13 @@ command:
- --cli-password=${TURN_CLI_PASSWORD:?} - --cli-password=${TURN_CLI_PASSWORD:?}
``` ```
Le compromis est explicite : les valeurs ne sont pas dans un fichier Le compromis est : les deux valeurs sont visibles dans `docker inspect
versionnable, mais elles sont visibles dans `docker inspect coturn` et dans la coturn` et dans la table des processus de l'hôte. Un secret Compose ferait
table des processus de l'hôte. C'est strictement mieux qu'un fichier mieux, coturn ne sait pas le lire.
versionné, ce n'est pas aussi bon qu'un secret Compose.
`turnserver.conf` ne doit donc jamais reprendre de lignes `turnserver.conf` ne doit jamais porter de ligne `static-auth-secret=` ou
`static-auth-secret=` ou `cli-password=` : ce serait à la fois un secret en `cli-password=` : ce serait un secret en clair dans le dépôt, et une seconde
clair dans le dépôt et une seconde source concurrente. source concurrente de celle du `command:`.
### Rotation ### Rotation
@@ -128,9 +126,15 @@ les appels échouent simplement à établir un relais.
```bash ```bash
sops edit secrets.env.yaml # TURN_STATIC_AUTH_SECRET sops edit secrets.env.yaml # TURN_STATIC_AUTH_SECRET
docker compose exec synapse vi /data/homeserver.yaml # turn_shared_secret
sops exec-env secrets.env.yaml 'docker compose up -d --force-recreate coturn' # homeserver.yaml s'édite depuis l'hôte : l'image Synapse n'a pas d'éditeur.
docker compose restart synapse 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 `macaroon_secret_key` (dans `homeserver.yaml`) signe les jetons d'accès : le
@@ -156,23 +160,19 @@ 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 déclaration, et Compose proposerait alors de le **recréer**. Ne pas toucher au
bloc `volumes:` du compose. bloc `volumes:` du compose.
### Pourquoi `POSTGRES_DB` est indispensable ### `POSTGRES_DB` conditionne le dump
`backup_docker.sh` détermine la base à dumper ainsi : `backup_docker.sh` lit la base à dumper dans l'environnement du conteneur
`db` :
```bash ```bash
db_name=$(docker exec "$container" printenv POSTGRES_DB 2>/dev/null || echo "postgres") db_name=$(docker exec "$container" printenv POSTGRES_DB 2>/dev/null || echo "postgres")
``` ```
Sans `POSTGRES_DB` dans l'environnement du conteneur `db`, `printenv` sort en Sans `POSTGRES_DB`, le repli s'applique : le script dumpe la base système
erreur, le repli s'applique, et le script dumpe `postgres` — la base système, `postgres`, vide, et journalise un succès. `POSTGRES_DB=synapse` dans
vide — avec succès et sans le moindre avertissement. Le dump obtenu fait l'`env_file` n'a pas d'autre fonction que d'être visible par ce `printenv`
quelques centaines d'octets et ne contient aucune table. PostgreSQL, lui, ne lit cette variable qu'à l'initialisation du cluster.
`POSTGRES_DB=synapse` dans l'`env_file` est ce qui garantit que le dump porte
sur la bonne base. La variable n'est lue par PostgreSQL qu'à l'initialisation
du cluster : elle est sans effet sur une base existante, sa seule fonction ici
est d'être visible par `printenv`.
Vérification, après tout changement touchant `db` : Vérification, après tout changement touchant `db` :
@@ -194,15 +194,38 @@ 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 — 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. sans erreur dans les logs, puisque du point de vue des serveurs tout va bien.
C'est déjà arrivé : `external-ip` a pointé pendant des mois sur ## Le chemin des requêtes clientes
`91.121.90.228`, l'adresse d'`admin.opytex.org`.
## Défauts connus `matrix.poneyworld.net` passe par Traefik, puis nginx, puis Synapse. Deux
conséquences à ne pas défaire :
**Trois images en tag flottant** : `nginx:latest`, - nginx doit propager la chaîne avec `$proxy_add_x_forwarded_for`, jamais
`livekit/livekit-server:latest`, `ghcr.io/element-hq/lk-jwt-service:latest`. `$remote_addr` : ce dernier vaut l'adresse de **Traefik**, et l'utiliser
La convention du dépôt veut une version en dur, et Renovate ne sait rien faire écrase l'en-tête posé en amont. Synapse voit alors tous les clients sous une
d'un `latest`. 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 : Références :

View File

@@ -5,8 +5,17 @@ name: matrix_poneyworld
services: services:
synapse: synapse:
image: matrixdotorg/synapse:v1.148.0 image: matrixdotorg/synapse:v1.159.0
restart: unless-stopped 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:
- env_file - env_file
volumes: volumes:
@@ -24,8 +33,10 @@ services:
- traefik.http.services.synapse.loadbalancer.server.port=8008 - traefik.http.services.synapse.loadbalancer.server.port=8008
db: db:
image: postgres:18 image: postgres:18.6
restart: unless-stopped restart: unless-stopped
security_opt:
- no-new-privileges:true
env_file: env_file:
- env_file - env_file
environment: environment:
@@ -33,6 +44,12 @@ services:
POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C
volumes: volumes:
- matrix_db:/var/lib/postgresql - 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: networks:
- default - default
labels: labels:
@@ -40,14 +57,29 @@ services:
backup.type: postgres backup.type: postgres
redis: redis:
image: valkey/valkey:8.1 image: valkey/valkey:9.1.1
restart: unless-stopped restart: unless-stopped
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 15s
timeout: 3s
retries: 20
networks: networks:
- default - default
nginx: nginx:
image: nginx:latest image: nginx:1.30.4
restart: unless-stopped 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: volumes:
- ./nginx/matrix.conf:/etc/nginx/conf.d/matrix.conf:ro - ./nginx/matrix.conf:/etc/nginx/conf.d/matrix.conf:ro
- ./nginx/www:/var/www/:ro - ./nginx/www:/var/www/:ro
@@ -60,11 +92,14 @@ services:
- traefik.http.routers.matrix_nginx.entrypoints=web-secure - traefik.http.routers.matrix_nginx.entrypoints=web-secure
- traefik.http.routers.matrix_nginx.rule=Host(`matrix.poneyworld.net`) - traefik.http.routers.matrix_nginx.rule=Host(`matrix.poneyworld.net`)
- traefik.http.routers.matrix_nginx.tls.certresolver=letsencrypt - 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 - traefik.http.services.matrix_nginx.loadbalancer.server.port=80
web: web:
image: vectorim/element-web:v1.12.11 image: vectorim/element-web:v1.12.26
restart: unless-stopped restart: unless-stopped
security_opt:
- no-new-privileges:true
volumes: volumes:
- ./element/config.json:/app/config.json:ro - ./element/config.json:/app/config.json:ro
networks: networks:
@@ -76,12 +111,15 @@ services:
- traefik.http.routers.element.entrypoints=web-secure - traefik.http.routers.element.entrypoints=web-secure
- traefik.http.routers.element.rule=Host(`chat.poneyworld.net`) - traefik.http.routers.element.rule=Host(`chat.poneyworld.net`)
- traefik.http.routers.element.tls.certresolver=letsencrypt - traefik.http.routers.element.tls.certresolver=letsencrypt
- traefik.http.routers.element.middlewares=crowdsec@file
- traefik.http.services.element.loadbalancer.server.port=80 - traefik.http.services.element.loadbalancer.server.port=80
livekit: livekit:
image: livekit/livekit-server:latest image: livekit/livekit-server:v1.13.5
command: --config /etc/livekit.yaml command: --config /etc/livekit.yaml
restart: unless-stopped restart: unless-stopped
security_opt:
- no-new-privileges:true
environment: environment:
# `keys:` a été retiré de livekit.yaml : la paire arrive ici, où # `keys:` a été retiré de livekit.yaml : la paire arrive ici, où
# l'interpolation ${...} de Compose s'applique. # l'interpolation ${...} de Compose s'applique.
@@ -103,10 +141,16 @@ services:
- traefik.http.services.livekit.loadbalancer.server.port=7880 - traefik.http.services.livekit.loadbalancer.server.port=7880
lk-jwt: lk-jwt:
image: ghcr.io/element-hq/lk-jwt-service:latest image: ghcr.io/element-hq/lk-jwt-service:0.6.0
restart: unless-stopped restart: unless-stopped
security_opt:
- no-new-privileges:true
depends_on:
- livekit
environment: environment:
LIVEKIT_JWT_BIND: ":8081" # 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_URL: wss://livekit.opytex.org
LIVEKIT_KEY: ${LIVEKIT_KEY:?} LIVEKIT_KEY: ${LIVEKIT_KEY:?}
LIVEKIT_SECRET: ${LIVEKIT_SECRET:?} LIVEKIT_SECRET: ${LIVEKIT_SECRET:?}
@@ -120,11 +164,13 @@ services:
- traefik.http.routers.lkjwt.entrypoints=web-secure - 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.rule=Host(`livekit.opytex.org`) && (PathPrefix(`/sfu/get`) || PathPrefix(`/get_token`) || PathPrefix(`/healthz`))
- traefik.http.routers.lkjwt.tls.certresolver=letsencrypt - traefik.http.routers.lkjwt.tls.certresolver=letsencrypt
- traefik.http.services.lkjwt.loadbalancer.server.port=8081 - traefik.http.services.lkjwt.loadbalancer.server.port=8080
coturn: coturn:
image: coturn/coturn:4.9 image: coturn/coturn:4.17.2
restart: unless-stopped restart: unless-stopped
security_opt:
- no-new-privileges:true
# Les deux secrets ne peuvent pas venir du fichier de conf : coturn n'y # 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. # interpole rien et n'offre aucune option `...File`. Voir README.md.
command: command:
@@ -134,6 +180,12 @@ services:
- --cli-password=${TURN_CLI_PASSWORD:?} - --cli-password=${TURN_CLI_PASSWORD:?}
volumes: volumes:
- ./coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro - ./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 # 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. # l'UDP en premier. 5349 (TURNS) reste en TCP seul, c'est du TLS.
ports: ports:

View File

@@ -5,7 +5,10 @@ server {
# Traefik -> nginx -> synapse # Traefik -> nginx -> synapse
location /_matrix { location /_matrix {
proxy_pass http://synapse:8008; proxy_pass http://synapse:8008;
proxy_set_header X-Forwarded-For $remote_addr; # $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; client_max_body_size 128m;
} }

View File

@@ -1,7 +1,7 @@
POSTGRES_PASSWORD: ENC[AES256_GCM,data:ooetYEZloaW9P13No7yOyuQ5CNOWRzCucgOjHLYlr8LJo09iJmbx1U8OiJU=,iv:v5SDUiN756EF+sIPx7OSCR/wYQ+isir9ZUaA5fzQikk=,tag:yA0Gso5WkoD8SirMeHlYsQ==,type:str] 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_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] 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:TunW0WK0Aurb2+G6gn2H9/gUUBx+YbdMqv487mIqENMQ8GVFgttY7q2194kbWkogLF0zaVTZChJsc8H66+wZmg==,iv:SYcOWq7oPRwgJmzPctwNkr+3TyRArI+jj1Smb9so30E=,tag:SJuSsncV/HIZ3+28lvHvaw==,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] TURN_CLI_PASSWORD: ENC[AES256_GCM,data:P1kj2dxT+BBHOWE3zqw82q/w8jrxIjZ1KCQbB05yWW2WjKBBvi1bP041E6sx1Xo7FOS035IX4dUcNo5wpS8Uzg==,iv:VDuQP8EXfc664cjMJJ7NOpvQhkKZF6BKKrTxfBRwjeQ=,tag:pblrz5eboJlz7dyjLqRv3w==,type:str]
sops: sops:
age: age:
@@ -23,7 +23,7 @@ sops:
fifz17xNS4S2YRCOVNal+AI4JXM9yw9hcoB4tt3ileEYcVYl9hzcHg== fifz17xNS4S2YRCOVNal+AI4JXM9yw9hcoB4tt3ileEYcVYl9hzcHg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1fh70nny5hzz8a8g9077kgfca0lhlh3wsjf4fjrv4klx5ucdtpdmsazuqdj recipient: age1fh70nny5hzz8a8g9077kgfca0lhlh3wsjf4fjrv4klx5ucdtpdmsazuqdj
lastmodified: "2026-08-19T16:23:53Z" lastmodified: "2026-08-19T16:33:04Z"
mac: ENC[AES256_GCM,data:7TpNSCfmHZ/oQOhWZS4KWiLCys8OceEuc9scRUj0q/cKDZfH5mtupFtEulZSgyx7C8pAoBpHzYeAk9UMxEXqOf4Xq8Uy3aS+HiFHCrMZASPzcDpMvfr9i39/b93eKGM8/Go9BN8lGDG1lCeenX1HCvftSltracfM+atFVfp8Wi0=,iv:BJElglNjb7ipcCX7XO8ONzK1UBZFunllPFp1W/hIabw=,tag:46GwhaHqla2U0lLPOCnGcw==,type:str] mac: ENC[AES256_GCM,data:Qh5v9F/F24goNXWwinfZskdtRGo5EY1eYwmKoBHb33wdrfQ+wTPjS1XaBnzkxOB9iqk5yna9GTwdqjaZSG/g4KgJRHUQpo+EQieuJn6LDK9YXSCyUGVwQd1ZeHwYypzJLb6JQ+qFwnYnZqOcFGIBGG2E8LFxU7+dFMdRGqn05eo=,iv:nfgJrbMvBQIUjGZoJHNjtv+Uy8fUfjQhxZ9zKcZShnI=,tag:OYAygJJRUkU0QHV7Y6lE7Q==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.13.3 version: 3.13.3

View File

@@ -77,23 +77,27 @@ exception : il n'est pas dans `env_file`, il vient de SOPS via
## `consume` et `export` ## `consume` et `export`
Deux bind mounts **relatifs à ce dossier**, donc situés dans le dépôt : | Volume | Rôle |
| Montage | Rôle |
|---|---| |---|---|
| `./consume` | tout fichier déposé ici est ingéré puis supprimé | | `consume` | tout fichier déposé ici est ingéré puis supprimé |
| `./export` | destination de `document_exporter` | | `export` | destination de `document_exporter` |
Ce sont des données, pas de la configuration : le `.gitignore` local les Ce sont des volumes Docker et non des dossiers de ce dépôt : les données
exclut du dépôt. Ne pas retirer ces règles, ce sont des documents personnels. 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.
Ces dossiers doivent appartenir à l'UID/GID déclaré dans `env_file` Déposer un fichier à ingérer passe par l'UI, ou :
(`USERMAP_UID` / `USERMAP_GID`, 1000) : Docker les crée en `root` s'ils
n'existent pas, et Paperless ne peut alors plus supprimer les fichiers qu'il a
consommés.
```bash ```bash
mkdir -p consume export && chown 1000:1000 consume export 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 ## Volumes
@@ -104,6 +108,8 @@ mkdir -p consume export && chown 1000:1000 consume export
| `paperless-media` | **les documents originaux et leurs archives PDF** | 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`) | | `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_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 trois premiers portent un `name:` explicite, sans préfixe de projet.

View File

@@ -23,7 +23,7 @@ services:
- backup.type=postgres - backup.type=postgres
webserver: webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:2.20.15 image: ghcr.io/paperless-ngx/paperless-ngx:3.0.5
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- db - db
@@ -59,10 +59,8 @@ services:
volumes: volumes:
- data:/usr/src/paperless/data - data:/usr/src/paperless/data
- media:/usr/src/paperless/media - media:/usr/src/paperless/media
# Bind mounts relatifs au dossier de la stack : leur chemin sur l'hôte - export:/usr/src/paperless/export
# change avec le déplacement du dépôt. Voir README.md. - consume:/usr/src/paperless/consume
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
networks: networks:
- default - default
- traefik-proxy - traefik-proxy
@@ -115,6 +113,13 @@ volumes:
# zéro plutôt que de convertir. L'ancien `paperlessopytexorg_redisdata` # zéro plutôt que de convertir. L'ancien `paperlessopytexorg_redisdata`
# subsiste, inutilisé : c'est le chemin de retour vers `redis:7`. # subsiste, inutilisé : c'est le chemin de retour vers `redis:7`.
valkeydata: 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: networks:
traefik-proxy: traefik-proxy:

View File

@@ -52,3 +52,11 @@ PAPERLESS_ADMIN_USER=waha-local
# Le provider lui-même est déclaré dans PAPERLESS_SOCIALACCOUNT_PROVIDERS, # Le provider lui-même est déclaré dans PAPERLESS_SOCIALACCOUNT_PROVIDERS,
# côté docker-compose.yml. # côté docker-compose.yml.
PAPERLESS_APPS=allauth.socialaccount.providers.openid_connect 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