Files
hq/02-DECISIONS/0166-the-container-runtime-is-a-node-seat-and-the-host-creates-containers-through-its-holder.md
T
jschoubben f1941304cc ADRs 0164-0166 and issue 190: the container runtime gets a module, a seat and declared settings
Proposed for the operator's review: settings declared with defaults and cost (0164),
container-runtime as a kernel capability (0165), node-container-runtime seat with the
host creating containers through its holder (0166), and the runtime's file written by
modules that are not its own (190).
2026-10-01 23:13:18 +02:00

12 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
what runs on it proposed 2026-10-01 jochen false 02-DECISIONS/0161-what-deserves-a-seat.md

166. The container runtime is a node seat, and the host creates containers through its holder

Context

Every container the mesh runs on a machine is created by the host, which looks for a runtime (docker info, then podman info) and drives that runtime's command line itself: run, inspect, remove, exec. Research 012 called this "detected rather than declared": the host takes over whatever runtime it finds. Nothing in the mesh owns the runtime. Its package came from the foundation bundle or was already on the machine. Its configuration file was written by hand, differs on each of the four machines, and is also written into by two modules that are not the runtime's (issue 190). Its service is declared by those same two.

The operator set the direction:

  • a module for the runtime, on every machine, owning "what is needed to run containers here": its packages, its configuration and its service;
  • that module holds a node seat for the runtime, so a second runtime (podman) can later compete for the seat;
  • the host stops speaking to the runtime directly and uses the seat's holder. The host stays the one that decides, and the holder becomes the one that executes;
  • every container on the machine is in scope, not only the mesh's. A development environment started by hand, or a test database a tool runs, is legitimate. The host already calls these strays: 3, 8 and 25 on three of the machines on the day of deciding;
  • the runtime's events and verbs are subjects on the bus, and the mesh's own interface is built on them. The third-party interface run until now was removed by hand.

ADR 0161's test for a seat is whether the mesh's own code finds it by name. Here it does: the host would look up the holder on its own machine. A singular role of a module held once per machine is a node-* seat (ADR 0121), in the controller's seed.

The constraint that decides most of this record is a cycle. The bus runs in containers. On the broker's machine, the broker's own container is created by the host. A holder's code served from a container cannot create the container that runs it. On a first machine, before the controller exists, nothing holds anything.

Considered Options

  1. The host calls the holder's verbs over the bus. Rejected: with the broker down, no machine can create any container, including the broker's. The mesh would be unable to restart its own transport.
  2. The holder picks a dialect that the host speaks itself, as with the uplink. Rejected: the host would still drive the runtime, and the module would drive it too for every other caller. That is two programs speaking to one daemon, and they come to disagree about the same machine (the installer's preflight already exists to avoid this).
  3. The holder's code runs as a supervised process on the machine and serves the seat's verbs twice: locally to the host, on the bus to everyone else. Adopted.

Decision

node-container-runtime is a seat of the mesh's own, node-scoped, in the controller's seed under this record. It delivers no provision; what it carries is its role's protocol: verbs its holder must serve (ADR 0132) and events its holder emits (ADR 0129). The runtime's module, docker, claims it and is assigned to every machine. A podman module may claim it later; one machine runs one.

The seat's verbs cover every container on the machine: list, inspect, logs, stats, start, stop, restart, create and remove. A mesh-held container is marked by the host's label and says which assignment holds it. Creating or removing a mesh-held container is the host's alone. Any other caller is refused naming the assignment, because the host would undo it at its next apply. Starting, stopping or restarting one is allowed, and the answer says the host will restore what its declaration says. A container the mesh does not hold is the caller's to do anything with.

The seat's events are the runtime's own — a container created, started, stopped, died, removed, its health changed. They are emitted on the seat's subjects, so every holder emits the same events and no reader depends on which runtime holds the seat. As ADR 0160 decides, the subjects are issued by the controller, not composed by the module.

The holder's code is a supervised process, not a container (ADR 0150). A runtime cannot be run by the thing it runs. The process serves the seat's verbs on the bus to the console, to tools and to the mesh's interface. The same verbs are served on a local socket on the machine, which only the host may use. The host creates, inspects and removes its containers through that socket and nothing else. If the holder does not answer, the host creates nothing. It says so in its report, naming the seat. It never falls back to the command line.

A container needs the seat held on its machine. An assignment that delivers a container on a machine whose runtime seat is unheld is refused, naming the seat and its candidates (ADR 0165). Mounting the runtime's socket into a container is granted by the seat, not by the capability. The socket's path is the holder's to state, because podman's is not docker's.

The runtime module owns the runtime's configuration. Its settings are declared with defaults (ADR 0164): the resolver containers use, the registries it trusts, log rotation, and keeping containers through a daemon restart. The module is given the resolver's address and the mesh's registry as values; no other module writes the runtime's file or declares its service.

The first machine is bootstrapped with the holder, and adopted afterwards. The foundation bundle already installs the runtime's package and service. It also carries the holder's process, delivered as a binary the way the host is (ADR 0142). When the runtime module is assigned, it adopts what the bundle made, as the store and broker modules adopt theirs (ADR 0078).

Consequences

  • The migration on the running mesh has a fixed order:
    1. The resolver module and the private network stop writing the runtime's file (issue 190). The runtime module takes the same file in the same push. Otherwise the controller refuses two modules declaring one path.
    2. The runtime module is assigned to every machine and adopts the runtime there. Each machine's hand-written configuration is read before the first push, because the module's defaults replace what differs.
    3. The controller seeds the seat and enforces the container requirement.
    4. The host releases the version that uses the holder.
    5. The host's command-line path is removed in the release after every machine's holder answers. Until then, the host reports per machine which path it used.
  • Every container the host makes depends on the holder's process. A crash-looping holder stops new containers on its machine. Running containers are unaffected. The host's report names the cause.
  • The process form of a module's own code must serve tools on the live mesh before this ships. Only the showcase declares it, and issue 117 found the showcase's tools declared in a form nothing runs. The runtime module is the first whose tools cannot fall back to a container.
  • A user interface subscribing to events directly does not exist. Today a reader of events is a module that consumes them. The mesh's container view is a module, or waits for that path.
  • The operator's decision to remove the third-party interface by hand needs no mechanism. No module-retires-module rule is introduced.
  • What got harder: the host gains a dependency it did not have, and a first machine's bundle gains a component. The direct path was simpler and is what makes a runtime a black box to the rest of the mesh.

How this is checked

Rule Checked by
The seat is the mesh's own, node-scoped, with its verbs and events A catalogue test on the default seats; registration refuses a claimant that does not serve every verb (design 33's existing check)
Creating or removing a mesh-held container is the host's alone A test of the runtime module's verbs: create or remove of a container carrying the host's label, from any caller but the host's socket, is refused naming the assignment; the same verbs on an unlabelled container succeed
The host uses the holder and never the command line A host test with a fake holder on the local socket: every container operation goes to it, and with the holder absent the apply creates nothing and reports the seat; after step 5, the host carries no command-line runtime code (checked by build: the package is gone)
A container needs the seat held A resolution test refusing a containerised assignment on a machine with the seat unheld, naming the seat
Socket mounts are granted by the seat A catalogue test: a module mounting the runtime's socket on a machine whose holder states a different path is refused
No other module writes the runtime's file The existing collision check, once the private network's computed resources are inside it (issue 190)
Live seats lists node-container-runtime held on every machine; the node listing shows each machine's containers, strays included, from the seat's list verb; a container started by hand appears as an event on the bus

References