feat(stacks): migrate paperless
This commit is contained in:
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/
|
||||
121
stacks/paperless.opytex.org/README.md
Normal file
121
stacks/paperless.opytex.org/README.md
Normal 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/
|
||||
117
stacks/paperless.opytex.org/docker-compose.yml
Normal file
117
stacks/paperless.opytex.org/docker-compose.yml
Normal 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
|
||||
54
stacks/paperless.opytex.org/env_file
Normal file
54
stacks/paperless.opytex.org/env_file
Normal 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
|
||||
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
|
||||
Reference in New Issue
Block a user