Skip to main content

Mooring Skipped the Orchestrator: Why a Typed YAML PaaS Still Hits a One-Machine Ceiling

17 min readDora NodaDora Noda
Share

A single typed mooring.yaml defines a multi-service app, Mooring generates and owns the Compose file and Dockerfile, provisions HTTPS at the edge, and monitors, alerts, backs up, and self-heals it — all from a single static binary with no Swarm or Kubernetes anywhere in the stack.

That is Mooring (daboss2003/mooring) in one sentence, and the sentence is deliberately narrow: one machine, owned completely. Give Mooring a Linux server with Docker and a Git repo containing a mooring.yaml, and a plain box becomes a place you can ship to in minutes — automatic HTTPS via Caddy, encrypted secrets, Git deploys with rollback, health dashboards, dead-man's-switch alerting, and an edge that just works. What the sentence does not say is equally deliberate: no declarative Machine lifecycle, no multi-node scheduling, no API that can provision a second server for you when the first one fills up. The same ceiling this blog has traced in Coolify, Dokploy, and CapRover — the gap a Cluster API fleet exists to close.

This post takes Mooring seriously on its own terms, names what "no orchestrator required" actually buys a solo dev, and then walks through the five concrete things that break at machine two — with a side-by-side that makes the single-box vs fleet line visible before you bet a roadmap on the wrong side of it.

Mooring in one screen: what "no orchestrator" actually ships

Mooring's pitch is not "Docker without complexity." It is "Docker with a control plane that has opinions, owns the hard parts, and documents its threat model." The comparison in its README — maintained as of 2026 against CapRover, Coolify, and Dokploy — makes the positioning explicit:

CapabilityMooringCapRoverCoolifyDokploy
No Docker Swarm required
No Kubernetes
Multi-service apps from one file⚠️ limited
Generates and owns Compose + Dockerfile
Automatic HTTPS at the edge
Built-in alerting + dead-man's switch⚠️ basic⚠️ partial⚠️ partial
Documented security / threat model⚠️ partial⚠️ partial⚠️ partial
Single static binary, no extra DB/services

Three choices in that table define Mooring's day-to-day feel.

1. A typed file is the API. You describe the app, not the plumbing:

yaml
apiVersion: mooring/v1
kind: App
metadata:
  slug: linkstash
spec:
  compose:
    source: generated            # Mooring generates & owns the compose
    services:
      api:
        build:                   # no Dockerfile to write — Mooring generates a hardened one
          context: .
        domains: [api.example.com]
      worker:
        build:
          context: .
          dockerfile: worker.Dockerfile
      db:
        image: postgres:16
        persist: db-data

You never hand-write a Compose file, never hand-edit a proxy config, never run certbot. Variants like mooring.staging.yaml and mooring.prod.yaml in the same repo become separate apps — one repo, several environments, no branching trick.

2. Security is a gate, not a toggle. The dashboard is loopback + IP-allowlisted, traffic is HTTPS-only, secrets are encrypted at rest and referenced by name (never sitting in plain text in a file, a repo, or a log), a push cannot auto-deploy itself, and an unsafe configuration makes Mooring refuse to start rather than run insecure. Trivy CVE scanning runs without mounting the Docker socket into the scanner — a detail that matters precisely because most PaaS dashboards treat host access casually. The security doc is not an afterthought page; it is the project's stated design center: hosting Mooring should never be the thing that gets your server hacked.

3. One binary owns the edge. Caddy at the edge, a health/monitoring loop, the Git deploy watcher, encrypted snapshots of Mooring's own state, and a self-update advisory checker all run inside a single mooring serve systemd unit. No Postgres to babysit for the control plane itself, no external state store, no asset pipeline. For a solo dev or a small team that wants Heroku-like git push ergonomics on hardware they own, that minimalism is the whole appeal — and it is genuinely well-executed.

On one machine, Mooring is arguably the most coherent single-box control plane the current wave has produced. The question is what happens when one machine stops being enough.

The five things that break at machine two

Self-hosted PaaS tools live or die not on how they run one box but on how they answer a tenant who says "I need a second box." The failure is never one dramatic outage. It is five quiet absences that compound:

1. No declarative Machine lifecycle

Mooring provisions nothing. You provision the server, install Docker, install the Mooring .deb, and hand Mooring a place to run. When that server fills up — CPU, memory, disk, or just the risk of a single point of failure — there is no Machine object to declare "I want a second node like this one," no controller to reconcile it, and no way to describe the desired fleet as YAML checked into Git.

What this looks like in practice: you SSH to a provider console, click "create droplet," install Docker, and decide manually which apps go where. Every scaling event is a human with a checklist, not a controller with a desired state. A Cluster API fleet inverts this: a Machine or MachineDeployment is a Kubernetes object on a management cluster, and a controller talks to the infrastructure provider (Hetzner, via CAPH, or any other CAPI provider) to create, replace, or roll the underlying VM. You kubectl apply a new node pool; you don't re-run the install docs.

2. No multi-node scheduling

Mooring owns one Docker Engine. Compose runs on that Engine. There is nowhere else for a service to go, no scheduler to place it elsewhere, and no notion of bin-packing across a pool. Scaling a stateless service means more replicas on the same box — which helps until the box itself is the bottleneck.

The concrete symptom: an API and a background worker that contend for memory on one host. On a fleet, a scheduler (Kubernetes, behind Cluster API) can pin the API to nodes with more CPU, the worker to nodes with more memory, and spread replicas across failure domains. On a single Docker host, the only tuning knob is the same-host resource limit, and the only failure domain is the box.

3. No API-driven fleet ops

Mooring's API and CLI manage apps, not machines. You can start, stop, restart, redeploy, mint tokens, and restore backups — all within one server. You cannot, from an API, ask the platform to provision hardware, cordon a failing node, drain its workloads, and replace it. That matters twice: for day-two operations (hardware anomalies, kernel upgrades, disk pressure) and for agent-driven operations, where an AI operator's whole ability to act depends on whether the machine-readable surface it is given can actually change the fleet.

A fleet API makes operations scriptable in the same way Git made deploys scriptable: every change is an API call, every state is observable, and an operator — human or agent — can close the loop without SSH. A single-box control plane, however polished, stops at the edge of the one machine it was installed on. Beyond that edge, the operator is back to a provider dashboard and a terminal.

4. No rolling upgrade across nodes

Mooring handles deploy-time rollback for an app — a bad mooring.yaml is checked before it goes live, with automatic rollback on failure. That is the right primitive inside one host. Across a fleet, the harder upgrade is not the app but the substrate: OS patches, Docker Engine updates, kernel or host-configuration drift. A fleet needs to roll nodes — cordon, drain, replace, verify — without downtime for tenants who did not ask for a maintenance window.

Cluster API's reconciliation model exists for exactly this: edit one field on a KubeadmControlPlane or MachineDeployment (for example, a Kubernetes version bump from v1.34 to v1.35), and controllers orchestrate the rolling replacement of underlying machines, with health checks gating each step. A single-box PaaS cannot roll a fleet it does not model, so the substrate upgrade remains a manual, per-host, per-tenant-downtime decision.

5. No provider abstraction

Mooring runs where you installed it. That is a feature for simplicity and a trap for portability. Moving a Mooring-managed app to a different provider, region, or instance family is a migration: new box, fresh install, restore encrypted snapshots, re-point DNS, verify. There is no provider interface to swap underneath the same declarative fleet description.

Cluster API's provider model (CAPH for Hetzner, CAPA/CAPG/CAPZ for AWS/GCP/Azure, CAPD for local Docker, plus dozens of community providers including UpCloud and others) decouples the workload description from the metal it lands on. The same Cluster and MachineDeployment shape, with a provider-specific InfrastructureMachineTemplate, can target different clouds without rewriting the app layer. For a team that chose "own the hardware" to escape vendor-specific PaaS lock-in, rebuilding that lock-in one bare-metal vendor at a time defeats the point — and a fleet API is the seam that prevents it.


Together, these five form a simple diagnostic. If every answer below is "no," the platform has a one-machine ceiling no matter how polished the one machine is:

  • Can I declare a new machine as YAML and have the platform create it?
  • Can a scheduler place my service on the machine where it fits best?
  • Can an API — not a human with SSH — provision, cordon, drain, and replace a node?
  • Can I roll the fleet's OS or Kubernetes version with a single field change?
  • Can I swap the infrastructure provider without rewriting the app?

Mooring answers no to all five by design. That is not a criticism of its implementation. It is the cost of the design choice its table advertises in the first row: no Swarm, no Kubernetes, no orchestrator — which is to say, no fleet.

Why "no orchestrator" is a tradeoff, not a virtue

The architecture diagram Mooring prints is admirably honest:

text
typed YAML  →  Mooring control plane  →  Docker Engine

There is no hidden Swarm mode, no embedded K3s, no etcd you forgot you were running. The control plane generates the Compose file and Dockerfile as implementation details you never hand-edit, then runs docker compose up / pull / build on your behalf. Scaling and self-healing are conservative, stateless-only, and opt-in. That honesty is why Mooring's security posture is easier to audit than a PaaS that mounts the Docker socket into half its services: fewer moving parts means fewer places to get privilege wrong.

The tradeoff is that everything Mooring owns, it owns on that one Engine. The compose file is generated; it is also, necessarily, a single-host compose file. The edge is Caddy; it is also a single-host edge with no cross-node load balancer to configure. Backups are encrypted snapshots of Mooring's own state; they are also snapshots of a single host's state, restored onto a fresh single host. The coherence that makes one box feel finished is the same coherence that makes two boxes feel like two separate Moornings that happen to share a Git repo — not a fleet.

Single-box tools often frame this as "you don't need Kubernetes." For the median solo app that statement is true and should be taken seriously — most apps never outgrow one good Hetzner box, and adding Kubernetes for a workload that fits on an AX42 is engineering theater. But the objection Mooring answers ("I don't want to operate Kubernetes for one app") is not the same as the problem a fleet solves ("I need to operate ten apps across three boxes with declarative, auditable, agent-operable lifecycle"). Collapsing the second into the first is where the ceiling is hidden: the moment the second need appears, "no orchestrator" stops being an answer and becomes the work you now have to do by hand.

Other tools in this niche try to soften the ceiling without removing it. Coolify adds multi-server via SSH targets, Dokploy leans on Swarm + Traefik, CapRover bolts on a multi-server mode — each gets you past one box, none gives you declarative Machines, a scheduler, or a provider interface. Mooring, to its credit, does not pretend to. Its README shows mooring serve and a systemd unit, not a fleet dashboard. The cleanest read is that Mooring chose to be the best single-box control plane it could be, not a compromised multi-box one.

Where a Cluster API fleet draws the line — and why at the control plane

A Cluster API fleet draws the multi-machine line one layer lower than a compose file: at the control plane that manages the machines themselves.

In CAPI's model, a management cluster (often a small, long-lived Kubernetes cluster) runs controllers for each infrastructure provider. A Cluster object declares the desired fleet; Machine and MachineDeployment objects declare the nodes; provider-specific templates (for example, HetznerMachineTemplate via CAPH) describe the hardware shape. The controllers reconcile desired state against the provider API — creating VMs, bootstrapping them with kubeadm, joining them to workload clusters, and replacing them when they drift or fail.

Concretely, the operator experience is GitOps-shaped:

yaml
# One field change rolls the fleet's Kubernetes version
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: KubeadmControlPlane
metadata:
  name: fleet-control-plane
spec:
  version: v1.35.0   # was v1.34.0 — CAPI does the rolling replacement
  replicas: 3
  machineTemplate:
    infrastructureRef:
      kind: HetznerMachineTemplate
      name: fleet-nodes-cpx31
yaml
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
  name: fleet-workers
spec:
  replicas: 6
  template:
    spec:
      version: v1.35.0
      infrastructureRef:
        kind: HetznerMachineTemplate
        name: fleet-nodes-cpx31

kubectl apply that change on the management cluster, and the fleet rolls — cordon, drain, replace, verify — without per-host SSH. Hardware anomalies get the same treatment: CAPI cordons the affected Machine, drains workloads to healthy nodes, tears down the faulty instance, and provisions a fresh one. No checklist, no dashboard click, no midnight provider-console session.

That is the line Mooring and its single-box peers intentionally do not cross. A fleet control plane does not compete with mooring.yaml's ergonomics; it competes with the assumption that ergonomics and fleet lifecycle must live in the same binary. The useful synthesis is layered: a fleet manager provisions and maintains the machines; an app platform (git-push, buildpack, App CR, managed edge) runs on top. Bex, for example, takes exactly this split — Cluster API for declarative fleet provisioning on Hetzner (and other CAPI providers), with a Render-compatible, git-push PaaS surface on top — so the tenant's git push never needs to know which Machine it landed on.

ConcernMooring (single-box)CAPI fleet + PaaS surface
App definitionTyped mooring.yaml, owns Compose + DockerfileGit repo or App CR, buildpacks/containers, declarative routing
Machine lifecycleYou provision the boxMachine/MachineDeployment reconciled by provider controller
PlacementOne Docker EngineScheduler across nodes and failure domains
Fleet opsSSH + provider console beyond one boxAPI-driven: provision, cordon, drain, replace, roll
Substrate upgradesPer-host, manualOne field change, rolling replacement
Provider portabilityReinstall and restore per boxSwap InfrastructureMachineTemplate, same fleet shape
Agent operabilityApp-scoped API, host-scoped beyond thatFleet-scoped API: machines, clusters, and apps all machine-readable

Choosing without ideology: when one box is enough

Not every team needs a fleet, and needing one later does not mean you needed one on day one. A practical filter:

SignalMooring-shaped answer fitsFleet-shaped answer fits
Number of appsOne to a handful, all fit on one box with headroomMany tenants or many services that contend for the same box
Availability target"Reboot and it comes back" is acceptable"A host can fail and tenants stay up" is required
Scaling unitScale services on one hostScale hosts and schedule across them
OperatorOne person who SSHesA team or an agent that needs an API for every operation
Provider strategyOne provider, one region is fine indefinitelyMight add a second region, family, or provider
Substrate churnRare host rebuilds, manual is fineRegular OS/Kubernetes rolls across the fleet

If the left column describes your next twelve months with confidence, Mooring's tradeoff is rational and its polish is real — you get a security-audited, single-binary PaaS with encrypted secrets and a generated, owned Compose layer that is nicer to operate than hand-maintained Compose or a thin Docker dashboard like Portainer. If any row in the right column is already true, or will be true when the next customer or the next app ships, the ceiling is not hypothetical and the migration cost after betting on a single-box tool is the real price of "no orchestrator required."

The ceiling is architectural, not personal

Mooring is a strong entry exactly because it is opinionated and well-bounded. A typed file that is the whole API, a control plane that owns the files it generates instead of asking you to maintain them, a security model that refuses to start when it would run insecure — those are the choices that make a single box feel finished, and the project earns the comparison it invites with Coolify, Dokploy, and CapRover on that ground.

The fleet gap is not a missing feature to be added in a point release. It is the consequence of the architecture that makes the single box so coherent: a control plane that talks to one Docker Engine cannot also be a control plane that provisions, schedules across, and rolls a fleet of Engines without becoming a different system — an orchestrator, with a scheduler, a Machine API, and a provider interface, which is to say, the thing it set out not to be.

Naming the ceiling plainly is not a dismissal. It is how a team chooses the right tool for the job it actually has — one finished box, or a fleet that has to outlive any one of them.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. A Cluster API fleet provisions the hardware, the PaaS surface handles the rest. Star the repo on GitHub or deploy your first app today.

Sources

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex