feat: add ChirpStack Cloudron package (Infrastructure) [#668]

ChirpStack 4.19.1 (LoRaWAN network server) as the 14th package:
official-image wrapper, digest-pinned, with only bash added to the
upstream alpine runtime. Native OIDC login wired to the Cloudron
platform provider (openid_connect backend, config regenerated every
start); postgresql + redis addons; diesel migrations auto-run.
Operator config (NetID, US915 region, gateway MQTT backend) lives in
seeded files under /app/data/config. Build green; config generation
validated through chirpstack's own TOML parser. Docs gardened to 14
packages.

Ticket: https://projects.knownelement.com/issues/668

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
2026-09-01 19:55:23 -05:00
parent 10e2b7c7ed
commit 0d7543704b
11 changed files with 570 additions and 11 deletions
+98 -1
View File
@@ -4,7 +4,7 @@
**Project**: TSYSDevStack-SupportStack-Cloudron
**Goal**: Package ~57 applications for Cloudron PaaS platform
**Start Date**: 2025-01-24
**Current Status**: 13/~57 packages completed (~23%)
**Current Status**: 14/~57 packages completed (~25%)
## Completed Packages
@@ -1038,6 +1038,103 @@ nothing)
---
### 14. ChirpStack (Infrastructure) ✅
**Application**: ChirpStack — open-source LoRaWAN network-server (web UI +
gRPC/REST on one port, PostgreSQL storage, Redis sessions/dedup, external
MQTT broker for gateways and integrations). Upstream:
https://github.com/chirpstack/chirpstack (MIT), v4.19.1.
**Ticket**: [#668](https://projects.knownelement.com/issues/668)
**Pattern**: official-image wrapper. A from-source build would drag the
whole Rust workspace + pnpm UI through a multi-GB compile; upstream ships
a supported image (`chirpstack/chirpstack`) whose final stage is alpine +
one static musl binary + ca-certificates, run as nobody:nogroup. Wrapping
it costs one `apk add bash` and an ENTRYPOINT override — 83.4MB final
image, the smallest package in the workspace so far.
**Auth gate verdict**: ✅ OIDC preferred. ChirpStack 4 has a native
OpenID Connect backend — `[user_authentication]` `enabled="openid_connect"`
plus `[user_authentication.openid_connect]` (provider_url, client_id,
client_secret, redirect_url, scopes; PKCE + nonce state stored in Redis).
`start.sh` regenerates this block on every start from
`CLOUDRON_OIDC_ISSUER` / `CLOUDRON_OIDC_CLIENT_ID` /
`CLOUDRON_OIDC_CLIENT_SECRET` with
`redirect_url = ${CLOUDRON_APP_ORIGIN}/auth/oidc/callback`.
**Key findings / decisions**:
- **Config model**: `chirpstack --config <DIR>` concatenates EVERY `*.toml`
in the dir (read_dir order is unsorted, so tables must be disjoint across
files — duplicates are a parse error) and substitutes `${ENV}` vars after
concatenation. Split into `10-cloudron.toml` (generated every boot:
logging, postgresql, redis, api, user_authentication — addon credentials
stay current across Cloudron password rotations) and operator-owned
`50-network.toml` + `region_us915_0.toml` seeded once, editable via the
file manager.
- **Migrations**: embedded diesel migrations run automatically in
`storage::setup()` at startup and seed an internal `admin` user
(email `admin`, password `admin`, is_admin). No manual migrate step.
- **Admin bootstrap gap**: users auto-registered via OIDC are non-admin,
and the internal login form is disabled in openid_connect mode. Document
path: `CHIRPSTACK_AUTH_MODE=internal` → login admin/admin → set a real
password + your SSO email → back to openid_connect. ChirpStack links an
OIDC identity to an existing user BY EMAIL, which transfers the admin
role to the SSO login.
- **API JWT secret**: `api.secret` signs login tokens; persisted at
`/app/data/.api_jwt_secret` so restarts don't invalidate sessions.
- **Ports**: single listener `api.bind 0.0.0.0:8080` (UI + gRPC + REST +
`/auth/oidc/*`). Gateways do NOT dial the app: ChirpStack 4 consumes an
external MQTT broker configured per region
(`[regions.gateway.backend.mqtt]`); US915 region file seeded as default
(Texas), operator points it at their broker.
- **Redis addon first use** in this repo: `CLOUDRON_REDIS_URL` feeds
`redis.servers` directly (auth embedded in the URL).
**Challenges & solutions**:
- **Hub API digest mismatch**: the Docker Hub tags API reported an index
digest for `4.19.1` that BuildKit refused (`not found` when used as
`tag@digest`). `docker manifest inspect --verbose` gave the real
registry digest (amd64 manifest `sha256:c74901…`); pinned
tag+that-digest and the build resolved. Lesson: trust the registry, not
the Hub API, when pinning.
- **Addon wait without clients**: the wrapper image has no psql/redis-cli,
and alpine package names drift between versions. Used bash `/dev/tcp`
probes instead — no extra packages, no version pinning headaches.
- **Secret escaping into TOML**: generated DSN/OIDC values pass through a
`toml_escape` (backslash + double-quote) helper; verified with an
adversarial password containing both characters — chirpstack's own TOML
parser accepted the generated files (run reached DB connect, i.e. past
config load, by design of the test).
**Verification**:
- `docker build --cgroup-parent ukrrs-batch.slice` green; image 83.4MB;
`chirpstack --version` → 4.19.1 inside the image.
- start.sh executed against a scratch `/app/data`: config + seeds written,
chirpstack parsed all TOML and proceeded to storage setup (failed only
at the intentionally absent DB — the expected boundary of a
no-addons smoke test).
**Files Created**:
- Dockerfile (official-image wrapper, digest-pinned, bash added)
- CloudronManifest.json (manifestVersion 2, port 8080, localstorage +
postgresql 16 + redis addons)
- start.sh (addon waits, JWT secret persistence, config generation,
seeding, exec) — committed executable
- README.md (auth story, admin bootstrap, config layout, MQTT note)
- CHANGELOG.md
- .env.example (CHIRPSTACK_AUTH_MODE / OIDC_REGISTRATION / LOG_LEVEL)
- .dockerignore (excludes the cloned repo/ from the build context)
- logo.png (from upstream ui/public/logo.png)
**Commit**: `feat: add ChirpStack Cloudron package (Infrastructure) [#668]`
---
## Packaging Pattern: Download Pre-Compiled Binaries
### When to Use