Skip to main content

Coolify, Dokploy, and CapRover Hit a Docker Swarm Ceiling

9 min readDora NodaDora Noda
Share
On this page

Every "self-hosted Heroku alternative" comparison post in 2026 ends up describing the same platform three times with different logos. Coolify, Dokploy, and CapRover each put a web UI in front of Docker, wire up one-click app templates, and get a solo developer from git push to a running HTTPS service on a $10 VPS in minutes. That part works, and it's genuinely good. What none of the comparison posts say plainly is where that architecture stops — the exact point at which "self-hosted PaaS" quietly means "one Docker host with a dashboard," and what happens the moment a workload needs more than one box.

The Docker Swarm Ceiling, in One Table

Here's the part the feature-checklist posts skip: what each tool actually requires and delivers once you go past a single node, set against a Cluster API-provisioned multi-node platform on the same axis.

CoolifyDokployCapRoverCluster API platform (e.g. bex)
Default topologySingle control-plane server orchestrating one or more independent Docker hostsSingle-node Docker Swarm by default (Swarm even with one node)Single Docker host; Swarm optionalMulti-node Kubernetes cluster from the first git push
Real multi-node requirementDocker Swarm mode is explicitly labeled experimental in Coolify's own docs; a true Swarm cluster needs 3+ same-architecture serversAdd worker nodes to the existing Swarm; supported but still Swarm's scheduling model underneathLong-standing Swarm support, but development pace has slowed and the UI hasn't kept upMachineDeployment scales worker capacity declaratively; adding a node is a manifest change, not a mode switch
Node-failure recoverySwarm restarts the failed service on a healthy node — no independent health-check-driven rollbackSame Swarm restart-on-failure modelSame Swarm restart-on-failure modelKubernetes reschedules affected pods in roughly 30 seconds, driven by a continuous control-loop health check, with rollback on failed checks
Persistent storage across nodesNeeds a bolt-on shared volume (NFS, GlusterFS, AWS EFS) for anything stateful to move between workers — not includedSame external shared-storage requirementSame external shared-storage requirementStatefulSet + PersistentVolumeClaim are first-class scheduling inputs, no separate storage layer to stand up
Tenant isolation boundaryNone below the OS — every app on a host shares one Docker daemon's blast radiusNone below the OS — same shared-daemon blast radiusNone below the OS — same shared-daemon blast radiusKubernetes namespaces + node affinity give each tenant a real scheduling and resource boundary
Idle control-plane cost~500MB–1.2GB RAM; a 2 vCPU/4GB VPS comfortably runs the dashboard for 10+ worker nodes~350MB RAM, ~0.8% idle CPU — the lightest of the threeComparable to Coolify, dated UI notwithstandingHigher baseline (control-plane components), amortized across the fleet it's actually managing

That table is the whole argument in miniature: Coolify, Dokploy, and CapRover are Docker orchestrators with a PaaS UI bolted on, and Docker Swarm — even where it's supported — was never built to be the multi-tenant, multi-node backbone a growing platform needs. That's not a knock on any of the three; Dokploy defaults to Swarm specifically so a single-node install can add workers later without an application rewrite, and Coolify's control-plane-plus-agents model keeps a 2 vCPU/4GB VPS comfortably in charge of ten or more workers. Both are sensible defaults for what these tools are actually for: getting one developer or a small team from zero to a running app fast, on hardware they already have. The rest of this post unpacks why each ceiling row actually bites once that team's workload outgrows a single trust boundary, and where a Cluster API-provisioned fleet earns the extra weight.

Three Things That Actually Break

Node failure recovery is a restart, not a reconciliation loop. When a Swarm worker goes down, Swarm's answer is to restart the affected service on a surviving node — a reactive, binary response with no independent judgment about why the container is unhealthy. Kubernetes runs a continuous control loop instead: the scheduler reschedules affected pods in about 30 seconds, and a Deployment that starts failing its own health checks can roll back automatically rather than getting restarted into the same failure. For a single side project, restart-on-failure is plenty. For a platform running other people's production traffic, "it restarted" and "it recovered correctly" are different guarantees, and only one of the two orchestration models makes that distinction.

Persistent storage needs a second system you didn't sign up to run. None of the three tools ship a way to move a volume between nodes on their own — Coolify's own multi-server docs are explicit that a shared volume (NFS, GlusterFS, AWS EFS) is required before a stateful service can move across Swarm workers, and Dokploy and CapRover inherit the identical Swarm limitation. Concretely: a Postgres container pinned to node A with a local volume simply doesn't come back if node A doesn't come back — Swarm can reschedule the service, but not the data directory sitting on that node's disk, unless the team has already wired up an NFS or GlusterFS export the volume can be remounted from. That's not a checkbox feature gap; it's a second piece of distributed infrastructure — with its own availability story, its own failure modes, and its own operational burden — that a team has to stand up and run before their database-backed app can survive losing a node. Kubernetes treats PersistentVolumeClaim as a native scheduling input from day one, backed by whatever CSI driver the underlying cloud or bare-metal fleet provides, so the storage layer is part of the same declarative model instead of a bolt-on a team discovers they need only after the first node loss.

There's no isolation boundary below "the whole host." Every app deployed through Coolify, Dokploy, or CapRover onto a given machine shares that machine's single Docker daemon. There's no namespace, no cgroup-enforced tenant boundary, no node-affinity rule keeping one tenant's noisy neighbor off another tenant's CPU — the blast radius of a misbehaving container, a resource-exhaustion bug, or a compromised dependency is "everything else running on this box." That's a reasonable trade for a single developer running their own five side projects. It stops being reasonable the moment a platform is running workloads for people who don't know or trust each other, which is exactly the scenario "self-hosted PaaS" starts implying once a team calls it a platform instead of a homelab.

What Cluster API Buys Instead

Cluster API is the Kubernetes SIG project that turns cluster and node lifecycle into the same declarative, reconciled resources application developers already trust for their own workloads. A KubeadmControlPlane manages the control-plane machines; a MachineDeployment manages a scalable pool of worker machines; both are just Kubernetes objects that a controller continuously reconciles toward the desired state — the identical model as a Deployment rolling out a container image, just one layer down at the infrastructure level.

That maps directly onto the three break points above. Node failure recovery stops being "Swarm restarts it and hopes" and becomes the same control-loop reconciliation Kubernetes already runs for pods, extended down to Machine objects — a node that fails health checks gets replaced, not just restarted. Persistent storage stops requiring a hand-rolled NFS cluster because PersistentVolumeClaim is native to the scheduler Cluster API is provisioning. And tenant isolation stops being "whichever host happens to run this container" and becomes namespaces plus node affinity — an actual scheduling boundary, not a shared daemon and good intentions.

None of this is free. A Cluster API-provisioned fleet carries a heavier control-plane footprint than Dokploy's 350MB, and it demands more Kubernetes literacy than clicking "deploy" in CapRover's UI. That's the honest trade, not a reason to pretend the trade doesn't exist.

Which One Do You Outgrow, and When

This isn't a shootout with a single winner — it's a question of which failure mode you're willing to accept, and for how long. Three concrete signals say the single-host model is done:

  • Sustained CPU or memory above 80% on the one box running everything. Below that line, Coolify or Dokploy on a single VPS is legitimately the faster, cheaper, lower-maintenance choice — don't provision a Kubernetes control plane to solve a problem you don't have yet.
  • You're running workloads for tenants who don't trust each other. The moment "self-hosted PaaS" means hosting other teams' apps, not just your own, the missing namespace boundary in Coolify/Dokploy/CapRover stops being an inconvenience and starts being a real security question.
  • A node failure needs to be survived, not just noticed. If losing one VPS for ten minutes while you SSH in and restart things is an acceptable outage, Swarm's restart-on-failure is sufficient. If it isn't — because customers are depending on uptime — the ~30-second automatic reschedule and health-check-driven rollback stop being a nice-to-have.

Hit any one of those and the honest move is to plan the migration before an incident forces it, not after — moving off a single Swarm host under outage pressure, with a half-built NFS export and customers already noticing, is a far worse day than doing it on a quiet Tuesday. Hit none of them, and a single-box Dokploy or Coolify install remains the right tool — multi-node Kubernetes solves problems most solo projects and small internal tools don't actually have, and there's no prize for provisioning a control plane you don't need yet.

A concrete version of the same call: a team running five internal tools for a ten-person company on one Hetzner box, with nobody outside the company touching those apps, has no isolation problem to solve — Coolify or Dokploy is correctly sized. A team selling hosting to other teams' production apps, where one tenant's traffic spike or bad deploy shouldn't be able to take down another tenant's site, has already crossed the isolation line the table above describes, whether or not they've had the incident yet that proves it.


Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on a Cluster API-provisioned, multi-node fleet of machines you own, from the first deploy rather than a later migration. Star the repo on GitHub or deploy your first app today.

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