Status: COMPLETE (2026-08-22, per Xore). The cutover finished and its
step 9 was executed in full: the Go dashboard and Python
es-results-importer service blocks are deleted from
honeypot-dashboard/compose.yml, every profiles: ["next"] line is gone
(the new tier is the default and only tier), and
scripts/cutover-dashboard.sh is removed. There is no runtime fallback
to the Go dashboard — falling back means checking out a pre-cutover
commit (any commit before this change) and running it normally, a
deliberate decision so no profile/override machinery lingers in the live
compose files. The rest of this document is the historical runbook the
cutover followed, kept for the record.
Status at drafting: first runbook for #1628. Unlike
KEYCLOAK-CUTOVER.md, this had not yet been executed
end-to-end — refine it against what actually happens the first time it's
run, the way that doc was.
Confirmed live against the homeserver (2026-08-20): the next profile
has never been activated. honeypot-dashboard's Arcane project reports
exactly 4 running services — dashboard, oidc-sessions,
es-results-importer, services-adapter, all legacy — and zero
hp-apiary-* containers exist anywhere on the host. dashboard-next,
backend-service, and every Rust worker (backend-worker,
backend-worker-importer, backend-worker-enrichment,
backend-service-mounted) have never run in production. This means:
no bake period has started for any worker, so none of #1628's
worker-retirement decisions can move to "retire" yet regardless of how
much parity testing has landed in CI — that testing proves the Rust
implementations are correct, not that they've run against real
production load. The actual next step, once #1628's remaining ops-
blocker items are resolved, is step 3 below (cutover-dashboard.sh preflight) for the very first time — not any worker's retirement.
Tracking issue for everything this cutover depends on: #1628. Do not start the procedure below until every item in that issue's "Deployment / ops blockers" section is checked off and the worker-retirement and feature-parity triage sections have an explicit decision recorded, not just this doc existing. A runbook does not substitute for the prerequisites it assumes.
Old: one Go binary (arcane/home/honeypot-dashboard/dashboard),
Arcane stack honeypot-dashboard, service dashboard, published on host
port 19090, fronted by VPS Traefik's honeypot-dashboard router
(vps/traefik/dynamic.yml). Background loops (notifyLoop,
reportScheduleLoop) run inside this same binary/service.
New: three tiers, all currently gated behind the next Compose
profile so nothing binds a host port or receives traffic until cutover:
dashboard-next— TanStack Start frontend/BFF (Arcane stackhoneypot-dashboard, same stack as the olddashboardservice, for now)backend-service— Rust request/response tier (split into its own Arcane stackhoneypot-dashboard-backend, #1622)backend-service-mounted,backend-worker,backend-worker-importer,backend-worker-enrichment— Rust workers and the sandbox/Ghidra/ GitHub-analysis submission tier (Arcane stackhoneypot-dashboard, same asdashboard-next)
Cutover is not a config flag. It is: bring the new tiers up standalone, verify them, repoint Traefik, move the port binding, then retire the old service and the old standalone worker stacks. There is no beta hostname today (#1628) — the new stack goes live at the same hostname the old one serves, which is why the pre-flight verification step below matters more than it would with a staged rollout.
None existed before this doc. The plan: do not delete the old
dashboard service definition or the old standalone worker stacks in the
same change that cuts over. Comment them out of active duty (stop the
containers, leave the Compose service blocks and their images in place)
for a bake period — one week is a reasonable starting point, adjust based
on what the bake period actually surfaces — before deleting anything.
Instant revert during the bake period is: re-point Traefik's
honeypot-dashboard service back to the old socat-hp-dashboard:8090
target, docker compose up -d dashboard (and the old standalone worker
stacks, if they were stopped), done. No data migration to reverse — both
tiers read the same Elasticsearch indices; the new tiers writing
alongside the old ones during the bake period is the point (see worker
retirement below), not a hazard.
Each step assumes the previous one is verified, not just completed.
Steps 2-4 and 6 (and their rollback) are automated by
scripts/cutover-dashboard.sh, run from the honeypot-dashboard stack
directory on the homeserver (honeypot-dashboard-backend is expected as
the sibling directory ../honeypot-dashboard-backend, matching how
Arcane's directory-aware sync materializes both) — see the script's own
header comment for exactly what it does and does not cover; it does not
touch Traefik (step 5) or make the step 7 validation/step 8-9
retirement calls for you.
- Confirm #1628's prerequisites are actually met, not assumed — walk its checklist literally, item by item.
- Set real values for every var
.env.exampledocuments as currently defaulting to empty in bothhoneypot-dashboardandhoneypot-dashboard-backend—DASHBOARD_SERVICE_TOKENabove all; an empty value there means the two tiers trust every request from anything else onhoneynet, not just each other. As of #2183 that consequence is mechanical, not prose-only: both tiers refuse to boot over an unset/empty token ([E-SERVICE-TOKEN]in the logs); the only sanctioned way around it is the dev-onlyAPIARY_ALLOW_UNAUTH_DEV=1(documented in each stack's.env.exampleand frontend-next/README.md). The preflight check in step 3 below remains as the belt to this suspenders. - Bring the new tiers up without touching Traefik:
./scripts/cutover-dashboard.sh preflight— brings up thenextprofile in both stacks (idempotent), hard-fails ifDASHBOARD_SERVICE_TOKENresolves empty in either, and health-checksdashboard-next/backend-service/backend-service-mounteddirectly over thehoneynetbridge (no host port ever published, so this step alone touches nothing external —dashboardis still the only thing Traefik reaches). - Verify from the homeserver directly: preflight's own health checks
cover
/healthz; still manually confirm a handful of golden-path pages SSR correctly,/api/livestreams, and login redirects to Keycloak and completes. Runport-tests/{backend-api,frontend-ssr, auth-flow}.shagainst this live instance if not already fresh. - Re-point Traefik — only if this cutover is ever cross-host; in
the current single-host topology the VPS-side
socat-hp-dashboardforward already points at a fixed home address (10.8.0.2:19090) regardless of which container answers there, so step 6's port move is what actually redirects traffic and no Traefik change is needed. If a future cross-host split does need this, changevps/traefik/dynamic.yml'shoneypot-dashboardserviceloadBalancertarget and deploy via.github/workflows/deploy.yml's own Traefik-config step (not something a homeserver script can drive). - Move the port binding:
./scripts/cutover-dashboard.sh cutover— re-runs preflight, stopsdashboard(frees host port19090; the container and its image are left in place for rollback — not yet removed), applies the port move todashboard-nextvia a generatedcutover.override.ymllayered on top of the checked-incompose.ymlrather than a live-only hand-edit of it, and waits fordashboard-nextto report healthy on the live port. If this sticks, move thatports:mapping intocompose.ymlitself in a real PR afterward and delete the override file — a live-only edit risks a later Arcane gitops-sync silently reverting it../scripts/cutover-dashboard.sh rollbackreverses this exactly: stopsdashboard-next's override-added binding, deletes the override file, and restartsdashboard(reusingdeploy-dashboard-rolling.sh's own Health.Status poll). - Full live validation: every golden-path page, every export/
download path, SSE, settings save, credentials/canarytokens actions,
report generation, auth (login, TOTP, logout, session expiry),
mobile nav if #1576's fix has landed by this point. Treat this the
same way
KEYCLOAK-CUTOVER.md's validation gates treat auth — no step here is optional because it "probably still works." - Stop, don't yet delete, the old tiers:
docker compose stop dashboardinhoneypot-dashboard, plus whichever standalone old worker stacks were decided (per #1628) to retire —honeypot-attacker-identity-worker,honeypot-agent-intrusion-worker, and the plaines-results-importerservice insidehoneypot-dashboarditself. This starts the bake period. - After the bake period, with no rollback needed: delete the
dashboardservice block fromcompose.yml, delete the retired worker stacks' directories (or their Arcane manifest entries, per however that decision was actually recorded), drop everyprofiles: ["next"]line from the remaining new-tier services, and remove the now-deadDASHBOARD_SERVE_MODE/cross-host-split code paths if #1622's cross-host split was never actually exercised in production by this point — or keep them if it was.
dashboardCompose service and its container, plusscripts/ deploy-dashboard-rolling.sh(superseded byscripts/ cutover-dashboard.shfor the cutover path — redeployingdashboard-nextafterward is still the plaindocker compose build <service> && docker compose up -d <service>docs/CI-CD.mdalready describes; that part didn't need its own wrapper)arcane/home/honeypot-attacker-identity-worker/stack, if its Rust replacement inbackend-workerwas confirmed at parity (#1628's worker-retirement decision, not automatic)arcane/home/honeypot-agent-intrusion-worker/stack, same condition- the plain Python
es-results-importerCompose service insidehoneypot-dashboard/compose.yml(distinct from its Rust replacement,backend-worker-importer), same condition - every
profiles: ["next"]line across both dashboard stacks - the old Traefik
socat-hp-dashboardforward, once nothing references it
Not removed by this cutover, decided separately: whether
DASHBOARD_SERVE_MODE's cross-host split (frontend and BFF on different
docker hosts, #1622's stated next step) is ever actually used. This
runbook assumes a single-host cutover throughout; a cross-host cutover
needs its own pass at step 5 in particular.
- Which of the ten feature-parity gaps (#1628) block step 7's validation vs. ship as an accepted v1 cut. Decide before step 1, not during step 7.
- Whether
notifyLoop/reportScheduleLoop's Rust replacements have run long enough dual-writing to trust on their own — the old versions retire with the rest ofdashboardin step 8/9, same as everything else in that binary. If that's premature for these two specifically, split them out before starting the procedure, not after.