Install
Markdown
Ask AI
Share

Docker Orchestration Without Kubernetes or SSH Scripts

How a reconcile loop, Docker's event stream and a reverse-dialed agent replace SSH and shelled docker commands, and what this design costs you.

Markdown
Ask AI
Share

If you run a handful of Docker services and want them kept alive without Kubernetes, you have two common options: scripts that SSH in and run docker, or a platform with its own control loop. This post explains the second design as Levelrail builds it, why we chose it, and what it costs. After reading you can judge whether a reconcile loop and an agent suit your setup, or whether a lighter tool fits better.

Checked on 2026-11-01 against the current Levelrail release.

Docker orchestration choices at a glance#

ChoiceInstead of
A reconcile loop driven by desired stateA script that runs commands per deploy
Docker’s event streamPolling docker inspect on a timer
An agent that dials out with mutual TLSSSH from the control plane
The Docker Engine APIShelling out to the docker CLI

How a level-triggered reconcile loop works#

Desired state lives in a database. A loop compares it with what runs and fixes the difference. The pattern comes from Kubernetes controllers.

The key word is level-triggered. An edge-triggered system reacts to events: “a container died, so restart it”. Miss the event and you never recover. A level-triggered system asks every time: what should exist, and what does exist? An interrupted pass does no harm, because the next pass re-derives the work from current state.

output
desired state (database)  --+
                            +--> diff --> act --> status condition + reason
observed state (Docker)   --+

Every pass ends by writing a status condition with a reason string, so the dashboard can say why something is unhealthy, not only that it is.

Writing reconcilers takes care. A controller cannot assume the container it is about to create does not exist, or that an operation from a moment ago finished. Each reconciler needs a test for the half-succeeded case: container created, start failed.

Why Docker events beat polling#

Observed state comes mainly from Docker’s event stream. A periodic resync runs as a backstop for events the stream missed. A system that polls every server on a timer burns CPU while nothing happens, and the cost grows with apps and servers. Reacting to events keeps idle work low.

What the other options look like#

OptionWhy not
Docker SwarmBuilding on its update semantics couples you to its limits
NomadBusiness Source License
Kubernetes or K3sBrings back the surface area this project exists to avoid
SSH plus shelled docker commandsPushes you toward polling or accepting gaps
One-shot SSH with no agentNo idle cost, but the transport becomes the failure point

Other self-hosted tools make different choices, and they suit different people. By their own documentation, Coolify and Kamal manage servers over SSH, Dokploy and CapRover build on Docker Swarm, and Dokku runs commands on the host with no daemon. Kamal has no server component, which gives it zero idle cost and the fewest moving parts. See the full comparison for a fair side-by-side, and verify any detail against each project, because they move fast.

How the reverse-dialed agent works#

On extra nodes, Levelrail runs levelrail-agent. It connects out to the control plane over gRPC with mutual TLS, and the control plane never opens a connection to a node. Three things follow:

  • Managed nodes need no inbound port for control traffic.
  • The setup works behind NAT.
  • The agent streams Docker events up instead of waiting for polls.

Enrollment uses a one-time join token that the agent trades for a client certificate. On a single server the agent runs inside the control plane process over an in-memory transport with the same interface, so one server and ten servers share a code path.

No shell out to docker#

Nothing in the reconcile path calls the docker CLI or SSH. A narrow runtime interface is the only way controllers touch containers, and the Engine API carries every call. That helps testing, because you can swap in a fake runtime that fails on command, with no Docker daemon. An API returns typed errors, while a command line returns strings that change between versions.

What this design costs#

  • A persistent agent is one more thing to install and keep running, where SSH-only tools need nothing on the target.
  • A control plane with its own state is more to back up than a tool with none. Levelrail keeps state in embedded SQLite files in one data directory.
  • Cross-node networking over the WireGuard mesh, the default multi-node path, runs so far only locally across two Docker daemons, not over a real WAN.

If you manage one box and want the least machinery possible, a smaller tool may serve you better. If you run a handful to dozens of services and want the platform quiet as that number grows, this trade fits.

What can go wrong#

SymptomCause and fix
Node shows offlineThe agent lost its session. Containers keep running because the node’s Docker owns them. Restart the agent.
Deploy fails on a remote nodeCheck the failure code with levelrail-cli apps deploys show <name>.
App unhealthy with no obvious causeRead the status condition reason: levelrail-cli apps status <name>.

What I have not verified yet#

The competitor descriptions above come from each project’s public documentation at the time of writing and from our own research notes. They may be out of date. I have not benchmarked idle CPU against any SSH-based tool.

FAQ#

Is this Kubernetes?#

No. It borrows the controller pattern, but runs one Go binary against the Docker Engine API.

Do I need an agent on a single server?#

No. The agent runs inside the control plane process.

What happens if the control plane is down?#

Containers keep running, because Docker owns them. Management stops until it returns.

Where next?#

See how this shows up in a deploy in zero downtime deploys for Docker apps. Reference: Architecture, Feature status, the reconciler ADR and the agent ADR.

Levelrail is open source under Apache 2.0. If this saved you time, a star on the repo helps other self-hosters find it.

Next steps

Keep reading

Zero downtime, with a way back Zero downtime, with a way back

explainer 5 min read

Zero Downtime Deploy for Docker Apps, With Rollback

How a Docker deploy waits for readiness before cutover, holds the previous release for rollback, rolls back on its own, and how to rehearse it.

  • Docker
  • Deployments
  • Rollback