1 Auto hebergement
az4aaz edited this page 2026-09-21 19:36:57 +02:00

Héberger sa propre instance

Trois conteneurs derrière un reverse proxy HTTPS. Aucun mot de passe n'est stocké : le SSO de l'école est rejoué à chaque connexion, les cookies PASS vivent dans Redis.

Le dépôt ne contient rien de l'instance publique : domaine, admins, secrets, tout passe par le .env.

Prérequis

  • Docker avec Compose 2.24 ou plus récent
  • Un domaine et un reverse proxy TLS. Les cookies sont Secure : en HTTP la connexion ne tient pas.
  • Un compte élève IMT Atlantique valide.
  • ~1 Go de RAM, 2 Go si on compile l'image sur place.

Démarrer

git clone https://git.az4aaz.xyz/az4aaz/paff.git
cd paff
cp .env.example .env && $EDITOR .env
docker compose up -d

docker compose up -d --build compile depuis les sources au lieu de tirer l'image publiée. Le service écoute sur 127.0.0.1:3000 (PAFF_BIND_ADDR, PAFF_PORT).

curl -I http://127.0.0.1:3000/
docker compose logs -f paff

Reverse proxy

pass.example.org {
    reverse_proxy 127.0.0.1:3000
}
location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

X-Forwarded-For n'est cru que si la connexion vient d'une IP de PAFF_TRUSTED_PROXIES (loopback + privés par défaut, ce qui couvre les bridges docker). Un proxy sur une autre machine doit y être ajouté, sinon le rate-limiting compte tout le trafic sur une seule IP.

Un proxy conteneurisé sur le même réseau docker peut viser paff:3000 et se passer du mapping de port.

Variables

Variable Défaut Rôle
PASS_URL https://pass.imt-atlantique.fr Base PASS
REDIS_URL redis://127.0.0.1:6379 Sessions + cache (imposé par le compose)
SHEET_ENGINE_URL http://sheetengine:3000 Conversion de la feuille en PDF. Vide : le lien PDF répond indisponible, le classeur reste téléchargeable
LISTEN_ADDR 0.0.0.0:3000 Écoute dans le conteneur
PAFF_PUBLIC_URL vide Repli quand une requête arrive sans Host
PAFF_BIND_ADDR / PAFF_PORT 127.0.0.1 / 3000 Publication du port
PAFF_TRUSTED_PROXIES loopback + privés CIDRs autorisés à envoyer X-Forwarded-For
SESSION_TTL 86400 Inactivité tolérée avant expiration (s). Repoussée à chaque usage
SESSION_MAX_AGE 604800 Plafond depuis la connexion (s), jamais repoussé
EVENT_CACHE_TTL 1800 Cache des événements (s)
DETAIL_CACHE_TTL 1800 Cache des détails (s)
GRADES_CACHE_TTL 900 Cache du relevé (s)
PAFF_ADMINS vide user_ids CAS admin, virgules
FICHES_ROOT /data/fiches Fiches partagées
SIGNAL_OUTBOX_TOKEN vide Bearer du relais Signal, vide = désactivé
PAFF_SIGNATURE_KEY vide Chiffre les tracés de signature (procédure dans .env.example)
ANALYTICS_SCRIPT_URL / ANALYTICS_WEBSITE_ID vides Télémétrie optionnelle
ANALYTICS_TAG vide Étiquette des mesures, si plusieurs instances partagent un Umami
TZ Europe/Paris Fuseau du conteneur
RUST_LOG paff=debug,info Verbosité

Durée des sessions : SESSION_TTL est une fenêtre glissante. Tant qu'une page PAFF est ouverte et à l'écran, elle le signale toutes les cinq minutes et l'échéance recule ; en profite aussi la session PASS, que le serveur relance au plus une fois par quart d'heure pour éviter d'avoir à refaire le pont au clic suivant. Rien n'est envoyé par un onglet en arrière-plan, et aucune tâche de fond n'entretient la session de quelqu'un qui n'est pas là. SESSION_MAX_AGE est le mur derrière : passé ce délai depuis la connexion, l'échéance ne recule plus et il faut se reconnecter.

Devenir admin : se connecter une fois, relever User ID: ... dans docker compose logs paff, le mettre dans PAFF_ADMINS, docker compose up -d.

Télémétrie : aucune par défaut. Sans ANALYTICS_SCRIPT_URL, aucune balise tierce et aucune origine externe dans la CSP. Les pages appellent malgré tout /static/analytics.js, qui ne fait alors rien : il n'émet que si un script de télémétrie a été chargé. Avec ANALYTICS_TAG, chaque mesure porte l'étiquette choisie, de quoi distinguer deux instances qui remontent au même endroit.

Umami 2.18 au minimum, 2.19 de préférence. La normalisation des URLs avant envoi n'existe qu'à partir de la 2.18, et c'est elle qui empêche un code d'UE ou le nom cherché dans l'annuaire de partir tel quel : en dessous, la recherche /annuaire?q=... arrive entière dans Umami, l'attribut data-exclude-search ne couvrant que les navigations internes et pas la page d'ouverture. La 2.19 ajoute l'envoi qui survit à la navigation, sans lequel la mesure d'une connexion réussie est perdue au moment où la page change.

Sauvegarder

Deux volumes : <projet>_redis_data (sessions, cache, assos, avis d'UE, plans) et <projet>_fiches_data. <projet> est le nom du dossier.

docker compose exec redis redis-cli SAVE
docker run --rm -v paff_redis_data:/data -v "$PWD:/backup" alpine \
  tar czf /backup/redis-$(date +%F).tar.gz -C /data .

Restaurer : tar xzf dans un volume vide, conteneurs arrêtés.

Mettre à jour

git pull
docker compose pull && docker compose up -d

Les sessions survivent (Redis en AOF).

Sans docker

redis-server --daemonize yes
cp .env.example .env
cargo run --release

Le binaire sert static/ depuis le dossier courant : le lancer depuis la racine du dépôt. Unité systemd minimale :

[Service]
User=paff
WorkingDirectory=/opt/paff
EnvironmentFile=/opt/paff/.env
ExecStart=/opt/paff/paff
Restart=on-failure

Limites

  • Ça ne marche qu'avec IMT Atlantique : CAS/SAML, SSO Moodle et le service iCal sont codés en dur (src/auth/, src/modules/). PASS_URL vise une autre instance Alcuin, le reste demanderait du travail.
  • Catalogue des UE et crédits ECTS = promo actuelle (crates/paff-core/src/grades.rs, scripts/scrape-catalog.js).
  • Rien n'est mutualisé entre instances : la vôtre repart avec un annuaire, des avis et des fiches vides.

Licence

AGPL-3.0. Une instance publique modifiée doit publier ses sources.