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
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-boothJWT_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. Whendevis confident-ready, mergedev → stage; CI builds:stage/:stage-<sha>; then bumpTAG=stage-<sha>inresources.tomlto that build and deploy from Core (manual, pinned — no webhook even on staging).main— vetted production booths::main-<sha>, manual + pinned.mainonly gets what survived staging.
The
TAG=stage-<sha>inresources.tomlis a pinned pointer: the moving:stagetag 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.tomlas a starting sketch:resources.tomlmirrors the workingpark-buziStack (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)
- 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. - 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.
- Secrets are per-booth and unique.
EVENT_SIGNING_KEYsigns 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). - Volumes preserved. The Stack must never run
compose down -v— that would wipe theparking-datavolume (the signed ledger). Komodo's "destroy" is gated for the same reason.