levelrail process dies outright (a panic, an OOM kill, kill -9, the host rebooting the process), what happens to your running apps and to node agents?
The answer has two halves, because the control plane is one process that owns two different things: the reconcile loop that manages containers, and an embedded Caddy instance (the reverse proxy that routes traffic to them). See architecture.md and ADR 005. The two halves fail differently, and this page measures both.
Everything below was measured against a real control plane binary, real Docker containers, a real node agent process, and a real SIGKILL.
For contributors: the tests behind this page
For contributors: the tests behind this page
test/e2e/reconcile/break_glass_control_plane_death_test.go and test/e2e/reconcile/break_glass_agent_reconnect_test.go. Run them yourself with go test -run TestBreakGlass -v ./test/e2e/... (needs a local Docker daemon).The crash-and-recovery timeline
What survives
A running app container is not a child of the control plane process. Docker owns its lifecycle, notlevelrail. When the control plane is killed:
- Every already-running container keeps running, unaffected.
- It keeps serving traffic directly on its published host port. Answering a request does not depend on the control plane process being alive.
- This holds for containers on the local node and for containers on a remote node through an enrolled agent. The agent owns the container through its own Docker connection, not through the control plane’s process tree.
What does not survive
Caddy’s routing dies in the same instant the control plane does, unless systemd holds the sockets. (With socket activation, the default for new installs, the listening sockets outlive the process and connections queue instead of being refused, see measured windows.) This follows from an architecture decision, not a bug: Caddy runs embedded inside the control plane process, not as a sibling container with its own lifecycle. No separate proxy process is left to keep serving while the control plane is down. Concretely: from the moment the control plane process exits to the moment a new one starts and reconciles ingress at least once, domain-based HTTPS routing is down. A request to your app’s domain fails during that window even though the container behind it never stopped. A request straight to the container’s published host port succeeds the whole time; a request through the domain does not. This is a real outage window. Its length depends on how fast the control plane process comes back (a process supervisor restarting it in seconds, versus a human noticing and runningdocker start by hand). The reconciler cannot shorten it, because the reconciler is down too.
In-flight reconcile work is lost, not resumed mid-step. If a deploy, a build, or a container removal was in progress when the process died, that specific in-flight operation does not continue from where it left off. It does not need to. Every controller here is level-triggered and idempotent (see architecture.md), so a restart works out what to do from scratch by diffing desired state against whatever Docker reports, the same as on any other reconcile pass.
What happens when it restarts
The restarted control plane runs a reconcile pass immediately on startup, without waiting for the resync tick. That pass reads desired state from the same SQLite database and observed state from the same Docker daemon it was managing before. Measured directly:- A container that is already running and healthy is left alone: not recreated, not restarted. Its restart count and container ID stay exactly what they were. The reconciler acts only when it sees a container that is missing or not running.
- Ingress resumes once the ingress controller reconciles again, which happens on that same first pass. The domain starts routing without touching the container behind it.
- If the container is on a remote node whose agent has not reconnected yet, that service is skipped for the pass instead of erroring or being torn down (logged as “skipping service for this reconcile pass: node transport unavailable”). It picks back up on its own once the agent’s reconnect loop finds the control plane again. You do not need to restart the agent.
Ingress availability windows (measured)
Measured locally on an Apple silicon laptop (real Docker, a realtraefik/whoami container, the embedded Caddy over HTTPS with HTTP/2) with a load generator sending 200 requests per second. The same scenarios on the 2 vCPU droplets earlier were slower (about 4s for a restart, 1s for a kill). Read these numbers for the shape and the relative improvement, not as what a small VPS will give you.
Where the windows cannot be removed:
- Without socket activation a restart still refuses connections for the length of the restart. Socket activation is on by default for new installs. An install that predates it needs
LEVELRAIL_SOCKET_ACTIVATION=1 ./install.sh upgrade(how). - Even with it, the instant the old process stops, a handful of TLS handshakes already in progress can fail (most likely Caddy unloading its certificate cache while the listener is still draining, which we did not confirm). Browsers retry these; a script without retries may see an error.
- A killed single-replica container still has a window of about the reconciler’s reaction time (a few hundred milliseconds locally, up to a second on a small server) in which requests get the friendly
503. Retrying the same address cannot help because the replacement usually gets a new port. Use two replicas for a service that must not drop requests. recreatedeploys are down by design while the old container stops and the new one starts. Visitors get the styled503and a page that reloads itself, not a dead connection or a TLS error.- A pinned host port still needs a stop-then-start swap, see deploy safety.
- A control plane crash (not a restart) with socket activation queues connections until
Restart=on-failurebrings it back, 5 seconds by default (RestartSec). Connections wait that long and may time out at the client.
What this page does not cover
- Data loss. This page is about a process crash where the disk is intact. If the disk holding
levelrail.dbis lost or corrupted, see disaster-recovery.md for off-box backups and the master key escrow you need to recover. - Multiple control plane replicas, or an external load balancer in front of the control plane itself. There is exactly one control plane process today, so there is no failover between two control planes to measure.
- A crash during the brief window
caddy.Loadis applying a new config. This page measures a crash of a system that has already converged, which is the common case. A crash mid-config-apply is a narrower race the tests above do not cover. - Multi-replica app failover during the ingress outage window. If an app has more than one replica, Caddy stops routing to all of them during the outage. This page does not measure whether one replica coming back before another changes anything, because ingress is down for all of them until the control plane restarts.
Deploys under live traffic
Measured on a 2 vCPU droplet running a build of this branch’s predecessor (v0.2.0-beta.15), with a load generator on a second droplet sending 50 requests per second over HTTPS through the ingress for the whole deploy. The request counts are real. The box was shared with other test workloads, so a few stray errors per run are not blamed on the deploy.
Fixes shipped from these runs: pinned host port handoff with restore on failure, removal of half-started containers, a check that a pinned port is actually published, rolling deploys keeping the routed replica until cutover, required secrets enforced at reconcile time, default load balancing for multi-replica apps, and ingress config applies skipped when nothing changed. These fixes are covered by unit tests with a fake Docker client; they have not yet been re-measured on a real server.
Not measured: Docker daemon restart mid-run and disk pressure (the shared test host could not be disturbed). Containers use restart policy
no, so after a daemon restart recovery depends on the reconciler’s next pass.
Next steps
Disaster recovery
What to do when the disk or the whole machine is lost.
Domains and ingress
Socket activation, on by default, makes a restart queue connections.