0015. Workloads run on Docker through the agent; nodes serve traffic as edges or through a relay
Status: accepted, 2026-10-10
Context
apps puts containers on nodes (ADR 0002: no Kubernetes; ADR 0008: agents dial out). Nodes differ: a
dedicated server can take ports 80 and 443 and serve its apps' domains itself; a server that already
runs something (its own reverse proxy on 80/443, say) can't, and some nodes sit behind NAT. The agent
needs a runtime it can drive without a cluster.
Decision
- Runtime: Docker Engine, driven by
xvii-node through the docker CLI (JSON output). Every
container and network the agent makes carries xvii.* labels; anything without them is left alone,
so a node can keep running what it ran before. Per-team bridge networks; containers never publish
host ports.
- Desired state is sent by
appsd over the agent's WebSocket, kept on the node
(/var/lib/xvii/agent/desired.json) and converged idempotently; workloads keep running while the
control plane is away.
- Ingress has two modes per node:
- edge: the node serves 80/443 itself (the agent, with Let's Encrypt certificates for its
workloads' hosts only); DNS for its apps' attached names points at it. Implemented 2026-10-10, as a mode a
node's manager switches; the relay keeps running beside it for
<app>.apps.<d>.
- relay: the control-plane server's Caddy takes the endpoints and forwards to the node's agent
proxy over HTTPS on a port of the node's choice, the node's certificate pinned by
appsd; the node
needs no 80/443 and only that port open to the control plane. Behind NAT a node can't be a relay
target; it runs workers and bots only (or joins a VPN).
- Node credential: a one-time join token (1 hour) is exchanged for a node id and secret; the agent
sends them on every connection;
appsd keeps only the secret's hash.
Consequences
- One more process per node (Docker) that the agent depends on;
xvii server join installs it when
missing (with the user's consent on a busy server).
- Relay traffic crosses the control-plane server: its bandwidth counts, and it is a single point for
relay nodes' endpoints.
- Adopting existing containers (stage 5, step 5) only needs labels and a reverse-proxy cut-over.