APEX #5 — ChirpStack : un serveur LoRaWAN dans l'armoire (et une impression de déjà-vu)
23 octobre 2026 · 6 min
#distech-controls,#eclypse-apex,#docker,#lorawan,#chirpstack,#serie-apex
Série ECLYPSE APEX — épisode 5/6 · Précédent : #4 Node-RED et l’API REST
Le LoRaWAN, en GTB, c’est souvent la même histoire. On pose des têtes thermostatiques radio, des capteurs de température… et toutes les données partent dans un cloud quelque part, avec un abonnement par capteur. Ça marche, mais quand le cloud tousse, c’est tout le bâtiment qui s’enrhume.
Aujourd’hui, on rapatrie tout ça à la maison : un serveur réseau ChirpStack complet, dans l’ECLYPSE APEX.

Et je vais être honnête avec vous : maintenant que vous avez compris le principe, c’est toujours pareil. Une image ARM64 avec un tag fixe, un port en 50xxx, ExposedPorts, Mounts, CpusetCpus, un volume pour les données, une config écrite au démarrage. Vous connaissez la chanson. Je ne vais donc pas vous redonner six JSON ligne à ligne : on se concentre sur ce qui change.
La troupe au complet
ChirpStack, ce n’est pas un conteneur, c’est une petite équipe :
| Conteneur | Image | Port APEX | Rôle |
|---|---|---|---|
chirpstack-redis | redis:7-alpine | 50379 | cache et sessions |
chirpstack-postgres | postgres:14-alpine | 50432 | devices, applications, profils |
chirpstack-init | alpine:3.20 | — | écrit la config, puis s’arrête |
chirpstack | chirpstack/chirpstack:4.12.1 | 50081 | le serveur réseau et son interface web |
chirpstack-gateway-bridge | chirpstack/chirpstack-gateway-bridge:4 | 50170/udp | reçoit les paquets des gateways, les publie en MQTT |
chirpstack-rest-api | chirpstack/chirpstack-rest-api:4 | 50090 | API REST et Swagger (optionnel) |
Plus le Mosquitto de l’épisode 2, qui sert de bus à tout ce petit monde. Comme d’habitude, tout le monde se parle via IP_APEX:port.
Ce qui change : pas de shell
Notre astuce fétiche, c’est le sh -c qui écrit la config au démarrage. Sauf que les images de ChirpStack et du Gateway Bridge sont distroless : pas de shell, rien. Le sh -c tombe à l’eau.
Ma première solution, je vous la donne pour l’anecdote : builder des images custom sur le PC, avec la config embarquée, les pousser sur Docker Hub, et les tirer depuis l’APEX. Ça marche, mais il faut Docker Desktop, un compte Docker Hub, et rebuilder à chaque changement de mot de passe. Bof.
La bonne solution est bien plus simple : un conteneur d’initialisation. Une petite Alpine, qui elle a un shell, écrit les fichiers TOML dans un volume, puis s’arrête. On monte ensuite ce même volume dans ChirpStack, à l’endroit où il cherche sa config. Le sh -c, mais chez le voisin.

{
"Image": "alpine:3.20",
"Entrypoint": ["sh", "-c"],
"Cmd": ["mkdir -p /cs /gwb && printf '%s\\n' '…chirpstack.toml…' > /cs/chirpstack.toml && printf '%s\\n' '…region_eu868.toml…' > /cs/region_eu868.toml && printf '%s\\n' '…gateway-bridge.toml…' > /gwb/chirpstack-gateway-bridge.toml && echo done"],
"HostConfig": {
"CpusetCpus": "0",
"RestartPolicy": { "Name": "no", "MaximumRetryCount": 0 },
"Mounts": [
{ "Target": "/cs", "Source": "chirpstack-config", "Type": "volume", "ReadOnly": false, "Consistency": "default" },
{ "Target": "/gwb", "Source": "chirpstack-gwb-config", "Type": "volume", "ReadOnly": false, "Consistency": "default" }
]
}
} Puis ChirpStack monte chirpstack-config sur /etc/chirpstack, avec "Cmd": ["-c", "/etc/chirpstack"] pour lui dire où lire sa config, et le Gateway Bridge monte chirpstack-gwb-config sur /etc/chirpstack-gateway-bridge. Aucune image custom : pour changer la config, on relance l’init, puis on redémarre ChirpStack et le Gateway Bridge.
Le contenu utile des TOML, en clair :
# chirpstack.toml
[postgresql]
dsn="postgres://chirpstack:CHANGEZ_MOI@IP_APEX:50432/chirpstack?sslmode=disable"
[redis]
servers=["redis://IP_APEX:50379"]
[network]
net_id="000000"
enabled_regions=["eu868"]
[api]
bind="0.0.0.0:8080"
secret="une-longue-chaine-aleatoire"
[integration]
enabled=["mqtt"]
[integration.mqtt]
server="tcp://IP_APEX:50083/"
json=true
# region_eu868.toml
[[regions]]
id="eu868"
description="EU868"
common_name="EU868"
[regions.gateway.backend]
enabled="mqtt"
[regions.gateway.backend.mqtt]
topic_prefix="eu868"
server="tcp://IP_APEX:50083"
# chirpstack-gateway-bridge.toml
[backend]
type="semtech_udp"
[backend.semtech_udp]
udp_bind="0.0.0.0:1700"
[integration.mqtt.auth.generic]
servers=["tcp://IP_APEX:50083"]
[integration.mqtt]
event_topic_template="eu868/gateway/{{ .GatewayID }}/event/{{ .EventType }}"
state_topic_template="eu868/gateway/{{ .GatewayID }}/state/{{ .StateType }}"
command_topic_template="eu868/gateway/{{ .GatewayID }}/command/#"
Deux pièges qui m’ont coûté des soirées :
- Un retour à la ligne après chaque valeur, et à la fin du fichier. Quand le TOML est écrit par un
printfdans unsh -c, un saut de ligne perdu ou un guillemet mal échappé colle deux lignes ensemble, et là le parseur plante. Vous cherchez longtemps. Une ligne vide entre les sections rend le fichier lisible, et ce genre d’erreur saute aux yeux. - Le préfixe de région doit être le même des deux côtés.
topic_prefix="eu868"côté ChirpStack,eu868/gateway/…côté Gateway Bridge. Si les deux ne parlent pas sur les mêmes topics, la gateway est « vue » mais ChirpStack ne reçoit jamais rien. Ambiance.
L’extension pg_trgm, toujours sans shell… ou presque
ChirpStack a besoin de l’extension PostgreSQL pg_trgm. Bonne nouvelle : l’image postgres:14-alpine, elle, a un shell. Retour au sh -c classique : on dépose un script SQL dans /docker-entrypoint-initdb.d/, que PostgreSQL exécute à la création de la base. Seulement à la création : si le volume chirpstack-pgdata existe déjà, modifier ce script ne change plus rien.
{
"Image": "postgres:14-alpine",
"Entrypoint": ["sh", "-c"],
"Cmd": ["mkdir -p /docker-entrypoint-initdb.d && echo 'CREATE EXTENSION IF NOT EXISTS pg_trgm;' > /docker-entrypoint-initdb.d/chirpstack.sql && exec docker-entrypoint.sh postgres -c listen_addresses='*'"],
"Env": ["POSTGRES_USER=chirpstack", "POSTGRES_PASSWORD=CHANGEZ_MOI", "POSTGRES_DB=chirpstack"],
"ExposedPorts": { "5432/tcp": {} },
"HostConfig": {
"CpusetCpus": "0",
"RestartPolicy": { "Name": "unless-stopped", "MaximumRetryCount": 0 },
"PortBindings": { "5432/tcp": [{ "HostPort": "50432" }] },
"Mounts": [
{ "Target": "/var/lib/postgresql/data", "Source": "chirpstack-pgdata", "Type": "volume", "ReadOnly": false, "Consistency": "default" }
]
}
} Le script n’est exécuté qu’à la création de la base, donc au tout premier démarrage. Et le volume est sacré, je ne vous le répète plus.
L’ordre de démarrage
Lui, il compte vraiment :
- Mosquitto, Redis et PostgreSQL ;
chirpstack-init, qui écrit la config et s’arrête (on peut le supprimer ensuite, les volumes restent) ;- ChirpStack ;
- le Gateway Bridge et l’API REST.
Puis http://IP_APEX:50081, login admin / admin… que vous changez immédiatement, évidemment. Côté gateway LoRaWAN : packet forwarder Semtech UDP, serveur = IP de l’APEX, ports montant et descendant = 50170.
Si ChirpStack reste en restarting, vérifiez dans l’ordre : PostgreSQL démarré, puis le contenu des TOML. Petit bémol côté logs : une image distroless écrit sur sa sortie console, et sans docker logs sur l’APEX, on ne les voit pas. Pas de > fichier.log possible non plus, puisqu’il n’y a pas de shell. Le réflexe de l’épisode 3 reste utile autrement : un FileBrowser monté sur chirpstack-config, le temps de relire les fichiers que l’init a vraiment écrits. Un saut de ligne perdu ou un mot de passe mal recopié s’y voit tout de suite.
Et les capteurs, alors ?

Une fois la gateway branchée, on crée une application, un device profile et ses capteurs dans ChirpStack. Le nerf de la guerre, c’est le codec du device profile, ce petit bout de JavaScript qui transforme les trames hexadécimales en JSON lisible.
J’en ai écrit et testé quelques-uns, par exemple pour la tête MClimate Vicki et la Milesight WT101. Ils sont en libre accès dans le décodeur LoRaWAN d’opengtb, où vous pouvez tester vos trames et récupérer le codec prêt à coller dans ChirpStack v4 :
Les uplinks décodés arrivent dans Mosquitto, sur application/+/device/+/event/up. Et pour en faire des points BACnet ? Toujours pareil, vous dis-je : Node-RED et l’API REST, comme à l’épisode 4. Seule la découverte change. Et côté LoRaWAN, le travail de Sylvain Montagny est la référence : son application LoRaBAC fait exactement ce pont, avec une bibliothèque de codecs et la prise en charge des automates Distech.
La suite
Vous l’avez remarqué : depuis trois épisodes, c’est toujours la même danse. Vérifier l’ARM64, choisir un port, écrire un JSON qui plaît à l’OPA, générer des mots de passe, démarrer dans le bon ordre, recommencer. Et quand c’est toujours pareil, un informaticien normalement constitué n’a qu’une envie : l’automatiser.
C’est exactement ce que j’ai fait. Au dernier épisode, je vous présente Open Apexpress : tout ce qu’on a vu dans cette série, en quelques clics.
Liens
$ gtb react
Cet article t'a parlé ?
$ gtb comments --tail
Commentaires
// chargement…