feat(stacks): migrate paperless

This commit is contained in:
2026-08-17 16:32:13 +02:00
parent 306667b6e1
commit 8fdf461ae1
5 changed files with 326 additions and 0 deletions

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

View File

@@ -0,0 +1,121 @@
# Paperless-ngx — paperless.opytex.org
Gestion documentaire (OCR + indexation). Cinq conteneurs : `webserver`
(l'application), `db` (PostgreSQL), `broker` (Redis, 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_redisdata`
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'
```
## 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`
Deux bind mounts **relatifs à ce dossier**, donc situés dans le dépôt :
| Montage | Rôle |
|---|---|
| `./consume` | tout fichier déposé ici est ingéré puis supprimé |
| `./export` | destination de `document_exporter` |
Ce sont des données, pas de la configuration : le `.gitignore` local les
exclut du dépôt. Ne pas retirer ces règles, ce sont des documents personnels.
Ces dossiers doivent appartenir à l'UID/GID déclaré dans `env_file`
(`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
mkdir -p consume export && chown 1000:1000 consume 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_redisdata` | file de tâches Redis | non sauvegardé, volontairement — reconstruit |
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.
`apache/tika:latest` est le seul tag flottant du dépôt. Il vient de la
documentation amont, mais il rend la stack non reproductible et invisible pour
Renovate — à figer sur la version réellement en service.
Références :
- https://docs.paperless-ngx.com/configuration/
- https://docs.goauthentik.io/integrations/services/paperless-ngx/

View File

@@ -0,0 +1,117 @@
services:
broker:
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- redisdata:/data
networks:
- default
db:
image: docker.io/library/postgres:16
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:2.13
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
# Bind mounts relatifs au dossier de la stack : leur chemin sur l'hôte
# change avec le déplacement du dépôt. Voir README.md.
- ./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.7
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:latest
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
redisdata:
networks:
traefik-proxy:
external: true

View File

@@ -0,0 +1,54 @@
# =============================================================================
# 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

View 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