--- status: resolved opened: 2026-10-03 located-in: - mesh-controller - mesh-host fixed-by: - mesh-host#86 - mesh-controller#252 - mesh-controller#253 - mesh-host#88 amended-design: --- # 213 — The controller is a Go program, and it still runs in a container ## What was observed 2026-10-03. The controller — the program that holds the `mesh-controller` seat and answers its verbs (`status`, `nodes`, `push`, `assign` …), composes every machine's declaration and plans the builds — runs on its machine as a container built from an image: ``` mesh-controller Up … (docker ps on the machine that runs it) ``` It is written in Go and compiles to one static binary, as the node host does. The host is delivered as a bundle and run as a process; the node's tool runtime now is too ([ADR 0193](../../02-DECISIONS/0193-every-bundle-the-runtime-serves-is-launched-and-the-runtime-knows-no-language.md)). The controller is the one piece of the mesh's own Go code still shipped as an image. ## Why it matters beyond this instance [ADR 0188](../../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md) §1 says a module's own code is bundles, never an image, and §3 that a bundle that is a service is a `process` the host runs. The controller breaks the rule it is the mechanism of: the registration gate that will refuse an image of a module's own code has to exempt the controller, or refuse it. It also costs what an image costs — a container runtime on its machine as a hard requirement, eight mounts standing in for files a process would simply read, an image rebuild for a binary change — and every restart of it is a container recreation, which is how the controller restarts in the middle of a plan today. ## What a fix has to settle - The controller's module declares a Go bundle (`system`, `binary`) and a `process` running it (`./`, mesh-host #81), with its credential and store connection as files and words, not container mounts and a container network name. - What the container gives it now that a process would not: it runs on the host's network already, as an unprivileged user (65534), with eight mounts. Each mount named and replaced by a path, and the user by an account the host declares. - The handover: the controller restarting itself as a process, on the one machine that runs it, without a window where nothing answers the mesh's verbs. Located only by owner; the move is a change of the controller's module and its deployment, not of its code. ## Fix prepared (2026-10-04) Three changes. mesh-host#86, merged: a process may name the container it `replaces`, and the host removes that container only once the process has stayed up across two checks. mesh-controller#252, awaiting the operator's merge: the controller's composition for a service process, and two controllers safe together for the handover — the second stands by on the controller's consumers until the first lets go, and all plan work holds one advisory lock. mesh-controller#253, held: the controller's manifest as a Go bundle and a process. It waits on [issue 223](../223-a-new-mesh-installs-its-controller-as-a-container/00-report.md), because with it a new mesh cannot be installed. ## Resolved Proven 2026-10-04 on the control machine: after the manifest change was merged and pushed, the host created the controller's account, ran its preparation step, started the controller as a process, found it up across both checks and removed the container. The controller now runs as its own account from its bundle, no controller container remains, its seat answered throughout, and the merge's own plan finished all three tiers under the new process. One fault on the way, fixed before it could leave two controllers or none: the host read the account not existing yet as a user database that did not answer — it matched the exit as text in a wording its own runner did not use — so the first apply stopped at the account and the container kept serving, which is the handover's safe failure (mesh-host#88).