Ğchange Essayer la démo web →

Déploiement serveur

Faire tourner un nœud, un relai et la passerelle sur un serveur (NixOS).

Contenu généré par IA. Cette page a été rédigée avec l’aide d’une IA et n’a pas encore été entièrement relue — elle peut comporter des imprécisions.

Cette page décrit un déploiement serveur complet à partir du flake Nix du dépôt : l’indexeur (trame-noded), le relai (trame-relay), la passerelle HTTP (trame-gateway, optionnelle), et le service des ressources statiques (le bundle web et ce site). Pour simplement installer un pod clé en main, voir plutôt Serveur.

Les composants

Composant Rôle Exposé sur
trame-noded nœud p2p : stocke et indexe la donnée, se synchronise via iroh rien en entrée (sort en UDP/QUIC)
trame-relay relai iroh pour les clients sans UDP direct (navigateur web, NAT strict) HTTP(S)/WebSocket
trame-gateway passerelle HTTP : sert un bloc par CID (/ipfs/<cid>), permaliens inspectables sans charger l’app ; découplée (store propre, résout les CID absents via le ticket du nœud) HTTP
bundle web + ce site fichiers statiques HTTP(S)

Le flake expose tout : packages.trame-noded / trame-relay / trame-gateway, les images OCI (oci-image, relay-oci-image, gateway-oci-image), packages.site, et les modules NixOS nixosModules.trame-noded / trame-relay / trame-gateway.

Prérequis (flake)

Ajoutez le dépôt comme entrée de votre configuration NixOS :

{
  inputs.trame.url = "git+https://git.duniter.org/HugoTrentesaux/datapod";
  # ... puis dans les modules de l'hôte :
  imports = [
    inputs.trame.nixosModules.trame-noded
    inputs.trame.nixosModules.trame-relay
  ];
}

Nœud indexeur (trame-noded)

services.trame-noded = {
  enable = true;
  # dataDir : état du nœud (blocs + index). Défaut /var/lib/trame-noded.
  network = "g1";              # étiquette de réseau gossip (topic des annonces)
  wan = true;                  # profil complet (relais + découverte iroh)
  admin = "<clé-publique-admin>";       # gouvernance : la seule clé de confiance à l'amorçage
  stateCar = "/chemin/genesis.car";     # état de référence initial (optionnel)
  follow = [ "<ticket-de-pair>" ];      # pairs à suivre (resync périodique)
  relay = "https://relay.exemple.org";  # se rend joignable via CE relais (clients web)
  ticketFile = "/run/trame-noded/node.ticket";  # publie son ticket (pour la passerelle / le build web)
  # servePmtiles = [ ./geopod-france.pmtiles ];  # fond de carte détaillé, servi en p2p (file CID)
  # listenPort = 4001; openFirewall = true;  # pour un port UDP fixe reproductible
};

Le nœud n’expose aucun port entrant à ouvrir : il sort en UDP/QUIC et passe par les relais iroh en wan. Options complètes : voir nix/trame-noded-module.nix.

Amorcer avec des données : stateCar ou carDir/importCars

Deux mécanismes distincts pour démarrer peuplé — ne pas les confondre :

Option Drapeau Effet
stateCar --state Adopte un CAR verbatim : son root devient le root du nœud (O(1), sans re-dérivation). C’est l’arbre entier initial — données + éventuelle charte d’admission signée déjà incluses → nœud conforme dès le boot. Le CAR doit être une racine de nœud (geopod-base).
carDir / importCars --car-dir / --import-car Fusionne des CAR de données brutes par-dessus l’état courant (chargement forcé, sans re-validation). N’apporte pas de charte — variante « données seules ». Accepte aussi un arbre d’index nu (geopod-index).

Règle simple : stateCar = état de référence complet (une seule racine adoptée), carDir = ajouter des données par-dessus. stateCar est appliqué avant les fusions. Voir aussi la génération de ces CAR.

Récupérer le CAR par hash (comme le bundle web). Le CAR de données est publié en asset de release (nix run .#publish-data-release -- <tag> — root et lignes par table dans les notes de la release). Côté infra, on le fetch par hash plutôt que de le garder dans le dépôt de config :

services.trame-noded.stateCar = pkgs.fetchurl {
  url = "https://git.duniter.org/HugoTrentesaux/datapod/-/releases/<tag>/downloads/geopod-data.car";
  hash = "sha256-…";   # hash de l'asset publié
};

Le nœud peut démarrer avant que ce CAR existe : il tourne alors à vide et se peuple dès qu’on branche le stateCar (les apps, elles, se synchronisent au fil de l’eau). Le root du manifeste est aussi le TRAME_ROOT de la flavor des apps.

Servir le fond de carte détaillé (.pmtiles)

L’app embarque un mini fond de carte (monde, basse résolution) en asset : il fonctionne toujours hors ligne, sans dépendance à OpenStreetMap. Un fond détaillé (zone plus précise, zoom plus élevé) peut en plus être servi en pair-à-pair par le nœud, sur le même modèle qu’un CAR de données :

services.trame-noded.servePmtiles = [ /srv/geopod/geopod-france.pmtiles ];

Le nœud ingère le fichier dans son block store (enveloppe trame/file, media type application/vnd.pmtiles, découpé en blocs) et le sert par son file CID — c’est ce CID que l’app résout par plage (node_file_range), pas une URL HTTP. Le CID s’affiche au démarrage du nœud (comme le CID d’un serveFiles) ; c’est cette valeur qui va dans TRAME_PMTILES_CID de la flavor des apps (flavors/README.md). Comme pour le CAR de données, l’app vérifie l’égalité entre le CID attendu (gravé dans la flavor) et celui réellement résolu — un fond non servi (CID absent) fait simplement retomber la carte sur le mini fond embarqué, jamais sur OpenStreetMap (retiré). Anciennes versions du fichier : pas encore garbage-collectées (comme serveFiles).

Récupérer le .pmtiles : scripts/init-bootstrap-pmtiles.sh

Le fond détaillé (~479 Mio) n’est pas dans le dépôt : il est publié en release GitLab, dans le registre de paquets générique (un asset direct de cette taille est refusé en 413) par nix run .#publish-pmtiles-release -- <tag> [chemin.pmtiles]. Côté serveur, le script d’initialisation le rapatrie, le vérifie et l’installe à l’emplacement attendu :

scripts/init-bootstrap-pmtiles.sh \
  --tag v0.0.8 \
  --sha256 <empreinte-sha256-du-fichier-publié> \
  --dest /srv/geopod/geopod-map.pmtiles

Le script n’installe que le fichier : il ne touche à aucun service. Il reste donc à (1) le déclarer — services.trame-noded.servePmtiles = [ "/srv/geopod/geopod-map.pmtiles" ] puis redémarrer le nœud, ou, en conteneur, déposer le fichier dans ./state et poser TRAME_SERVE_PMTILES=/state/geopod-map.pmtiles (compose.turnkey.yaml) — et (2) vérifier l’égalité des CID :

journalctl -u trame-noded | grep -i pmtiles   # file CID servi

Ce CID doit être byte-à-byte TRAME_PMTILES_CID de flavors/geopod.json. S’il diffère, les clients retombent silencieusement sur le mini fond embarqué : mettre à jour la flavor avant de rebuilder les apps (elles gravent la valeur au build). Le CID est stable d’une release à l’autre tant que le .pmtiles ne change pas.

Relai (trame-relay) — deux montages

Le relai parle HTTP/WebSocket. Deux cas :

A. Le serveur dédie 80/443 au relai (TLS natif)

Le relai gère lui-même le certificat :

services.trame-relay = {
  enable = true;
  hostname = "relay.exemple.org";
  tlsCert = "/var/lib/acme/relay.exemple.org/fullchain.pem";
  tlsKey  = "/var/lib/acme/relay.exemple.org/key.pem";
  openFirewall = true;         # ouvre 80/443
};

B. Un reverse-proxy occupe déjà 80/443 (recommandé ici) — sous-domaine dédié

C’est le cas quand Caddy (ou nginx) tient déjà 80/443 sur l’hôte. Un sous-domaine dédié au relai règle le conflit : le relai tourne en plaintext sur un port local, et le reverse-proxy termine le TLS et relaie le WebSocket.

services.trame-relay = {
  enable = true;
  httpBind = "127.0.0.1:8090";  # plaintext, local uniquement
  # pas de tlsCert/tlsKey, pas d'openFirewall : c'est Caddy qui expose
};

Caddy (le sous-domaine relay.exemple.org → relai local) :

relay.exemple.org {
    reverse_proxy 127.0.0.1:8090
}

Caddy obtient le certificat automatiquement et transmet le WebSocket de façon transparente ; les clients (navigateur) se connectent en wss://relay.exemple.org. C’est cette URL qu’on met dans le TRAME_RELAY de la flavor de l’app web.

Passerelle HTTP (trame-gateway, optionnelle)

Utile pour des permaliens inspectables https://…/ipfs/<cid> sans charger l’app (le modèle par défaut étant que le navigateur fait tourner un nœud WASM).

Modèle découplé (ne PAS partager le répertoire du nœud). La passerelle ouvre son propre répertoire de données, qu’elle doit pouvoir écrire : à l’ouverture, elle y pose ses schémas d’amorçage et son fichier root. Un montage/dossier en lecture seule échoue (Read-only file system, puis Permission denied) — c’est le piège à éviter. Elle ne lit pas le store du nœud : pour servir un CID qu’elle n’a pas, elle le résout via iroh en suivant le ticket du nœud (peers / peerFile), rapatrie le bloc dans son propre store, puis le sert. Nœud et passerelle sont donc deux services indépendants reliés par le réseau, pas par un dossier.

Course au démarrage. peerFile n’est lu qu’une fois, au boot. Démarrez la passerelle après que le nœud a écrit son ticket : pointez services.trame-noded.ticketFile et services.trame-gateway.peerFile sur le même chemin, et ordonnancez l’unité passerelle après le nœud (after). Sinon la résolution réseau reste désactivée jusqu’au prochain redémarrage.

Module NixOS (nixosModules.trame-gateway), derrière Caddy :

services.trame-noded.ticketFile = "/run/trame-noded/node.ticket";  # le nœud publie son ticket

services.trame-gateway = {
  enable = true;
  # dataDir : store PROPRE, inscriptible. Défaut /var/lib/trame-gateway (géré).
  peerFile = "/run/trame-noded/node.ticket";  # suit le ticket du nœud pour résoudre les CID
  bind = "127.0.0.1";               # local ; Caddy expose
  publicHost = "ipfs.exemple.org";  # isolation par sous-domaine <cid>.ipfs.<host>
};

# Démarrer après le nœud (le ticket doit exister quand la passerelle boote) :
systemd.services.trame-gateway = {
  after = [ "trame-noded.service" ];
  wants = [ "trame-noded.service" ];
};
ipfs.exemple.org {
    reverse_proxy 127.0.0.1:8480
}

Image OCI équivalente : gateway-oci-image (monter un /data inscriptible, passer le ticket du nœud en --peer/--peer-file, publier le port 8480).

La passerelle n’alimente pas la démo web (décision 2026-07-25) : l’app web est un client p2p, elle se synchronise par le relai (section suivante), pas par HTTP. La passerelle reste utile pour les permaliens par CID (pont IPFS, partage d’un bloc à qui n’a pas de client).

Le code garde un repli par passerelle — adoption creuse + fetch paresseux : le bundle adopte un root creux puis rapatrie les blocs un par un en GET /ipfs/<cid hex 32 o>?format=raw sur l’URL de la flavor (TRAME_GATEWAY_URL). Il est non activé : TRAME_GATEWAY_URL est vide dans les flavors publiées, et la renseigner détourne le boot vers ce chemin au lieu du p2p. Si on l’active un jour, l’app étant servie depuis un autre domaine, la réponse doit porter Access-Control-Allow-Origin: * — c’est le cas depuis 2026-07-25 (trame-gateway, réponses brutes) ; veiller à ce qu’un reverse-proxy ne filtre pas cet en-tête ni ne le duplique.

Servir le bundle web et ce site (statiques)

Les deux sont des fichiers statiques :

exemple.org {
    root * /var/www/site        # nix build .#site
    file_server
}

app.exemple.org {
    root * /var/www/web         # geopod-web.tar.gz décompressé
    file_server
    try_files {path} /index.html
}

Le build web (nix run .#build-web) est actuellement impur (toolchain nightly + réseau, hors sandbox Nix) : côté infra, s’appuyer sur l’asset geopod-web.tar.gz de la release (fetché par hash) plutôt que sur une dérivation. Une dérivation pure est prévue.

Chaîne bout-en-bout : navigateur → relai → nœud

Un navigateur n’a pas d’UDP direct : pour qu’une app web rejoigne un nœud, le relai sert de point de rendez-vous WebSocket. Les trois briques se câblent ainsi :

  1. Le nœud s’annonce via le relai. services.trame-noded.relay = "https://relay.exemple.org" : le nœud se rend joignable par ce relai. Il publie aussi son ticket (ticketFile).
  2. Le relai termine le TLS derrière Caddy. Il tourne en plaintext local (httpBind = "127.0.0.1:8090"), Caddy expose wss://relay.exemple.org (section Relai B).
  3. L’app web est buildée pour une flavor (relai + ticket du nœud figés au build). On assemble flavors/geopod.json (TRAME_RELAY = https://relay.exemple.org, TRAME_BOOTSTRAP = <ticket-du-nœud>, TRAME_NETWORK, TRAME_ROOT indicatif) puis nix run .#build-web (défaut : cette flavor). Le ticket est celui écrit par le nœud dans ticketFile. Voir flavors/ et Démo web. En dev seulement, nix run .#build-web -- none retombe sur --dart-define=GC_SYNC=<relais>|<ticket> au runtime.
navigateur (app web, nœud WASM)
   │  wss://relay.exemple.org   (flavor: TRAME_RELAY | TRAME_BOOTSTRAP, figés au build)
   ▼
Caddy ──► trame-relay (127.0.0.1:8090, plaintext)
   │  relaie le canal iroh
   ▼
trame-noded  (relay = https://relay.exemple.org, joignable via le relai)

Résultat : l’app web démarre peuplée (sync initiale via le relai) et se met à jour, sans qu’aucun port UDP ne soit exposé.

Schéma d’URL de relai : https://, jamais wss://. Une URL de relai iroh se déclare en HTTP ; le client en dérive lui-même wss:// pour ouvrir le WebSocket, et interroge <relai>/ping en HTTP pour mesurer la latence. Écrite en wss://, cette sonde devient wss://…/ping et échoue en navigateur, où elle passe par fetch (URL scheme "wss" is not supported — constat 2026-07-25). Les valeurs héritées en wss:// sont désormais normalisées par le cœur (trame_net::normalize_relay_url), mais écrire https:// reste la forme correcte, dans la flavor comme dans services.trame-noded.relay.

Le ticket du nœud doit être produit alors qu’il est en ligne sur le relai. Le ticket ne porte l’URL du relai que si le nœud était déjà connecté au relai quand il l’a écrit (--relay <url> + online(), ce que fait trame-noded). Un ticket produit sans --relay ne contient que des adresses IP, inutiles à un navigateur : regénérer TRAME_BOOTSTRAP depuis un nœud lancé avec le relai. (Le client web réinjecte par sécurité l’URL du relai de sa flavor dans l’adresse du pair, mais le nœud doit bel et bien être joignable par ce relai.)

Remettre la démo web en route (procédure opérateur)

Cette section est le mode opératoire de mise en production de la démo web (https://demo.gchange.gyroi.de) : elle suppose le nœud, le relai et la passerelle déjà décrits plus haut. Pour un auto-hébergeur qui monte sa propre démo depuis zéro, voir Démo web ; ici on ne traite que la remise en route de l’instance publique, dans l’ordre.

Le chemin de données de la démo est la sync p2p : la page est un nœud complet (cœur en WebAssembly) qui rejoint le réseau par le relai en WebSocket, faute d’UDP en navigateur, et se synchronise depuis le nœud d’amorçage. Elle ne dépend d’aucune passerelle HTTP. D’où l’ordre : relai et nœud joignables d’abord, flavor ensuite, build en dernier.

1. Vérifier que le relai est joignable en wss depuis l’extérieur

Le relai doit répondre en HTTPS et accepter le passage en WebSocket. La sonde de latence que fait le client est un simple GET /ping, qui doit porter Access-Control-Allow-Origin: * (le relai iroh le met lui-même) :

curl -sI -H 'Origin: https://demo.gchange.gyroi.de' https://relay.datapod.gyroi.de/ping

Attendu : 200 + access-control-allow-origin: *. Si le reverse-proxy filtre cet en-tête, la sonde échoue en navigateur. Vérifier aussi que Caddy laisse passer l’Upgrade: websocket vers 127.0.0.1:8090.

2. Vérifier que le nœud est en ligne sur ce relai, et regénérer son ticket

Le nœud doit tourner avec services.trame-noded.relay = "https://relay.datapod.gyroi.de" (schéma https, pas wss : voir l’encadré de la section précédente). Au démarrage il journalise :

[noded] relais explicite : https://relay.datapod.gyroi.de (joignable via relais)
[noded] en ligne sur le relais
[noded] ticket écrit dans /run/trame-noded/node.ticket

Le ticket écrit après « en ligne sur le relais » est le seul qui porte l’URL du relai — c’est celui à reprendre dans la flavor. Un ticket produit sans relai ne contient que des adresses IP, inutilisables depuis un navigateur.

3. Mettre à jour la flavor, avant le build

Dans flavors/geopod.json :

"TRAME_RELAY": "https://relay.datapod.gyroi.de",
"TRAME_BOOTSTRAP": "<ticket courant du nœud>",
"TRAME_GATEWAY_URL": "",

Ces champs sont gravés dans le bundle à la compilation (--dart-define-from-file) : les modifier après le build n’a aucun effet, il faut rebuilder. TRAME_GATEWAY_URL reste vide : la renseigner détourne le boot vers le repli par passerelle (fetch paresseux) au lieu du p2p. Vérifier au passage la cohérence des autres champs (TRAME_ROOT, TRAME_STATE_CID, TRAME_PMTILES_CID) : voir flavors/README.md.

4. Rebuilder et redéployer le bundle web

nix run .#build-web          # utilise flavors/geopod.json par défaut
# → app/build/web/ : à publier tel quel sur le domaine de la démo

Publier le contenu de app/build/web/ sur demo.gchange.gyroi.de (statique, try_files {path} /index.html). Le build web est impur (Flutter nightly + wasm-pack, hors sandbox Nix) : pour une release, l’asset geopod-web.tar.gz fetché par hash est équivalent — à condition qu’il ait été construit après l’étape 3.

Critère de succès

Ouvrir la démo, console du navigateur ouverte, vider le stockage du site (sinon on relit l’état d’une visite précédente) :

Échecs typiques :

Récapitulatif DNS

Nom Cible Sert
exemple.org Caddy → .#site le site de présentation
app.exemple.org Caddy → bundle web l’app web (nœud WASM)
relay.exemple.org Caddy → 127.0.0.1:8090 le relai iroh (wss)
ipfs.exemple.org Caddy → 127.0.0.1:8480 la passerelle (permaliens par CID, optionnel)

Le nœud indexeur, lui, n’a pas besoin d’entrée DNS entrante.