Compare commits
11 Commits
e6a8ad6547
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| aab8280ff8 | |||
| dbf771515f | |||
| dea4604828 | |||
| 0e301e76bd | |||
| 3f6a3475c8 | |||
| 3844100012 | |||
| 4ad4bb59c6 | |||
| 948358971d | |||
| 1b221dfec8 | |||
| 885c3b62ea | |||
| c7147957b1 |
@@ -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"
|
||||||
|
}
|
||||||
|
|||||||
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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 là : 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.
|
||||||
|
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
|
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 :
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user