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--manifest <fichier|url>remplace--sha256si l’empreinte est publiée dans un manifeste au formatsha256sum(la ligne du fichier demandé est retenue).- Le téléchargement est forcé en HTTP/1.1 : en HTTP/2, un gros asset peut être tronqué silencieusement sans erreur (constaté sur la 0.0.7). Le script refuse d’installer un fichier dont l’empreinte ne correspond pas.
- Écriture atomique (fichier temporaire puis
mv) et idempotente : relancé avec la même empreinte, il ne retélécharge rien. GITLAB_TOKENn’est nécessaire que si le projet est privé ;--host,--project,--package,--fileet--urlcouvrent les autres instances.
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 serviCe 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.
peerFilen’est lu qu’une fois, au boot. Démarrez la passerelle après que le nœud a écrit son ticket : pointezservices.trame-noded.ticketFileetservices.trame-gateway.peerFilesur 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=rawsur l’URL de la flavor (TRAME_GATEWAY_URL). Il est non activé :TRAME_GATEWAY_URLest 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 porterAccess-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 :
- Ce site :
nix build .#site→ un dossier HTML à servir tel quel. - Bundle web : l’asset
geopod-web.tar.gzde la release (récupérable par hash), à décompresser et servir statiquement.
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’assetgeopod-web.tar.gzde 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 :
- 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). - Le relai termine le TLS derrière Caddy. Il tourne
en plaintext local (
httpBind = "127.0.0.1:8090"), Caddy exposewss://relay.exemple.org(section Relai B). - 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_ROOTindicatif) puisnix run .#build-web(défaut : cette flavor). Le ticket est celui écrit par le nœud dansticketFile. Voir flavors/ et Démo web. En dev seulement,nix run .#build-web -- noneretombe 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://, jamaiswss://. Une URL de relai iroh se déclare en HTTP ; le client en dérive lui-mêmewss://pour ouvrir le WebSocket, et interroge<relai>/pingen HTTP pour mesurer la latence. Écrite enwss://, cette sonde devientwss://…/pinget échoue en navigateur, où elle passe parfetch(URL scheme "wss" is not supported— constat 2026-07-25). Les valeurs héritées enwss://sont désormais normalisées par le cœur (trame_net::normalize_relay_url), mais écrirehttps://reste la forme correcte, dans la flavor comme dansservices.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 faittrame-noded). Un ticket produit sans--relayne contient que des adresses IP, inutiles à un navigateur : regénérerTRAME_BOOTSTRAPdepuis 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/pingAttendu : 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émoPublier 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) :
sync-demo: démarrage sync via https://relay.datapod.gyroi.de;- puis
sync-demo: statut=…:1en cours,3= données reçues (root avancé),2= déjà à jour,4= échec ; - annuaire, annonces et carte se remplissent, en lecture seule ; l’état est rangé en IndexedDB, donc un rechargement repart de là (et ne synchronise plus qu’un diff).
Échecs typiques :
statut=4immédiat avec, en console, une erreurfetch/WebSocket sur l’URL du relai : relai injoignable, TLS/Upgrademal proxyfié, ou URL écrite enwss://dans une build antérieure au correctif du 2026-07-25 ;statut=4après quelques secondes : le nœud d’amorçage n’est pas en ligne sur ce relai, ouTRAME_BOOTSTRAPest un ticket périmé — reprendre à l’étape 2.
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.