Files
parking_solution/komodo
julian dfc5a07c10
Build & push images / images (push) Successful in 2m56s
CI / check (push) Successful in 44s
Retire the park-lab stack from art-docker-station
That host is becoming a Traefik edge, and parking's prod override brings its own
Caddy on `network_mode: host` holding :80 — the two cannot share the port. The
lab tier moves to a dedicated bench PC rather than contorting either side.

This also names what has been holding :80 on that box: the edge stack deployed
there on 2026-09-01 failed with "address already in use" and the owner was
recorded as unidentified. It was almost certainly this Caddy.

REMOVING THIS BLOCK DOES NOT STOP ANYTHING. The containers keep running and keep
the port. Destroy park-lab from Komodo Core BEFORE syncing this removal:
DestroyStack names a stack and Core resolves where from its own synced copy of
the definitions, so a sync that drops the block first takes the teardown handle
with it. If that has already happened, remove the containers by hand on the host
— there is no compose project context on a Komodo-managed box.

Three Core secrets are now unreferenced: art_docker_station_jwt_secret,
art_docker_station_event_signing_key, art_docker_station_backup_key. Lab keys
with no real ledger behind them, so they are safe to delete once the stack is
gone.

Claude-Session: https://claude.ai/code/session_01SARfPK19vLBstMWBxubezN
2026-09-01 11:33:22 +02:00
..

komodo/ — fleet deployment as code

Infra-as-code for the Komodo Core control plane that deploys the parking appliance to the booth fleet over the NetBird mesh. See wiki/decisions/fleet-deployment-komodo.md for the rationale, threat-model analysis, and the three settled choices (many/growing fleet · deploys are manual + pinned · secrets are Komodo-managed, per-booth).

This directory does not change how images are built or how the app runs — it's only the control plane. The booth still runs the same docker-compose.yml + docker-compose.prod.yml (container-deployment); Komodo just drives them remotely instead of someone SSH-ing in to run booth.sh.

Files

  • resources.toml — the Komodo resource definitions (Servers, Stacks, optional Builders/ Procedures), synced into Core via a ResourceSync. This is the reviewable, version- controlled source of truth for which booth runs what.
  • .env.komodo.example — the variables a Stack expects, documenting what comes from Core's secret store (per-booth JWT_SECRET / EVENT_SIGNING_KEY / BACKUP_KEY) vs. plain Stack env.

Promotion: dev → stage → main

Three tiers (see wiki/decisions/fleet-deployment-komodo.md):

  • dev — the working branch. CI builds :dev / :dev-<sha>. No booth deploys off it.
  • stage — what the staging booth (park-buzi) runs, to test the app in real-world conditions. When dev is confident-ready, merge dev → stage; CI builds :stage / :stage-<sha>; then bump TAG=stage-<sha> in resources.toml to that build and deploy from Core (manual, pinned — no webhook even on staging).
  • main — vetted production booths: :main-<sha>, manual + pinned. main only gets what survived staging.

The TAG=stage-<sha> in resources.toml is a pinned pointer: the moving :stage tag exists but we deploy the immutable sha so a booth runs a known image. Re-pin on each promotion.

How Core consumes this (one-time)

In Komodo Core, create a ResourceSync pointing at this repo + path (komodo/resources.toml), on the branch you manage from (e.g. main). Core reads the file and reconciles Servers/Stacks to match. Thereafter, a PR to this directory + a sync is how you change the fleet — no clicking.

Komodo's TOML schema evolves across releases. Treat resources.toml as a starting sketch: resources.toml mirrors the working park-buzi Stack (built by hand in the Core UI, then exported to TOML — so field names match the running Komodo version, v2.2). Import it into the sync Unmanaged first and review the diff; it should be ~empty against the live Stack.

How servers get created — NOT here

There is no [[server]] block in resources.toml. Servers are created by the Periphery agent onboarding outbound: in Core, create a one-time Onboarding Key (Settings → Onboarding), then install Periphery on the booth passing --onboarding-key + --core-address (Core's reverse-proxy URL, reached over the NetBird mesh) + --connect-as=<booth-name>. The agent self-registers, generates its own auto-rotating key pair (private key never leaves the booth), and connects outbound — the booth opens no inbound port. The sync owns only the Stack, which references the server by the name it onboarded as (server = "park-buzi"). See wiki/decisions/fleet-deployment-komodo.md.

Adding booth N

Copy the [[stack]] block, change name, server (its onboarded name), and the per-booth secret references ([[park_<site>_jwt_secret]], [[park_<site>_event_signing_key]], [[park_<site>_backup_key]]). Create those secrets in Core's store first. Set branch + TAG for the tier the booth runs (staging → stage / stage-<sha>; production → main / main-<sha>).

Hard rules encoded here (do not relax without updating the decision page)

  1. No deploy webhook on a booth Stack. Deploys are a human action; pin an immutable TAG=<branch>-<sha> (staging → stage-<sha>, production → main-<sha>). A moving tag on a booth is the non-determinism we rejected — and we hold that line even on the staging booth.
  2. Onboarding, outbound, mesh-only. Servers self-register via an onboarding key; Periphery connects outbound to Core's mesh URL and exposes no inbound port. Never a LAN/WAN address.
  3. Secrets are per-booth and unique. EVENT_SIGNING_KEY signs the anti-fraud ledger — one leak must taint one booth, never the fleet. Reference Core secrets by name; never inline a real value in this file (it's in git).
  4. Volumes preserved. The Stack must never run compose down -v — that would wipe the parking-data volume (the signed ledger). Komodo's "destroy" is gated for the same reason.