HeroCtl vs Kamal: when you need more than one server
Kamal is brilliant for a VPS running Rails. When the second serious customer asks for redundancy, the architecture has to change. Where Kamal stops and HeroCtl starts.
In 2023 37signals published numbers that didn't match the industry's narrative. The company left the public cloud to host its own products on dedicated servers, projected approximately three million dollars in annual savings, and open-sourced the tooling it used to make that migration. That tooling got a name — Kamal — and quickly became a reference for a kind of team that was tired of the complexity of planetary orchestrators.
DHH, partner at 37signals, popularized the thesis around the tool with a short phrase: "you don't need orchestration". The argument is elegant, factual, and — for most teams it describes — true. Rails app deploy had become a master's degree in distributed systems without anyone asking for it. Kamal is the legitimate response to a legitimate frustration, and occupies its own niche within the self-hosted Heroku segment in 2026.
This post isn't about taking down that thesis. It's about the exact moment it stops describing your reality. Somewhere between the first and the tenth serious customer, the phrase changes from "you don't need orchestration" to "you started needing it and didn't notice yet". The symptom is usually an email at three in the morning.
The Kamal philosophy, as it deserves to be described
Before any criticism, worth recording what Kamal gets right — because it gets a lot right.
You have an application inside a Dockerfile. You have a list of servers where you want it to run. Kamal takes that list, connects to each server via SSH, pulls the new image, swaps the old container for the new, and updates the internal router. There's no persistent control plane. There's no resident agent on each server with its own process. There's no state database outside the set of servers you already had.
The configuration lives in a single deploy.yml file with maybe forty lines. You run a command, and Kamal does the work in parallel on the hosts. When it finishes, it disappears — there's no service listening on port 8080 waiting for future commands. It's deploy as an SSH transaction with checkpoints.
That minimization of external state is the central virtue. For teams of one to three people running a monolithic application on a single server, it's hard to beat. Coolify and the other modern panels ask for their own database, their own agent, their own web interface. Kamal asks for two ingredients: SSH and Docker. You already had both.
We estimate that seventy-five percent of web app teams in Brazil in 2026 never need more than that. If you're one of them, close this post and install Kamal. Seriously.
When "you don't need orchestration" is true
Some markers are honest. If your operation fits in this list, Kamal beats any comparison you can make against it:
- A single server with resource headroom
- Monolithic application, no internal dependencies in private network between services
- Predictable traffic, no spikes that demand emergency horizontal scaling
- Occasional deploy window, with up to thirty seconds of degradation acceptable
- End customer who doesn't charge a formal availability contract
- One to three people taking care of infra, half-time
For this profile, any tool with a persistent control plane is unjustified overhead. Kamal is, literally, a configuration file plus a client binary. There's nothing to maintain on the server beyond the Docker you'd install anyway.
The elegance of this approach is such that many teams with a profile slightly larger than the one described above remain happy — and they're right. Migrating to an orchestrator before the pain appears is infrastructure gold-plating. The right question isn't "do I have a cluster?", it's "what pain can I no longer ignore?".
When "you don't need orchestration" starts to hurt
The pain doesn't arrive all at once. It arrives in four stages, generally in this order.
First stage: the customer demands an SLA
At some point a service contract enters with an availability clause. The most common numbers are 99% (3.65 days of permitted downtime per year), 99.9% (8.7 hours), and 99.95% (4.4 hours). The customer wants to see the number in the contract.
Here Kamal on a single server no longer fits. Not because Kamal is bad — it's because a single server never gives 99.9% without external maneuver. Cloud provider maintenance, disk failure, kernel update: each of these events fully consumes your annual downtime budget. In 2026 no serious provider guarantees 99.9% uptime for an individual instance.
The natural response is "I'll put two servers". And that's where Kamal starts to need external scaffolding.
Second stage: two servers and the cluster illusion
Kamal accepts a two-IP list in deploy.yml without complaint. But what it does with that list isn't orchestration — it's repetition. The two servers are parallel deploy destinations, not members of a cluster.
Concretely: if one of the two falls, Kamal doesn't reallocate traffic. Kamal has no opinion on traffic between deploys. You need to set up, on the side:
- A load balancer (cloud provider or self-hosted)
- Periodic health check that takes the inactive server out of the balance
- DNS resolution configured on top of the balancer, not the direct servers
- Some notification mechanism when something goes off
That's four new products you start operating. Each has its own panel, its own account, its own bill, its own way to break at three a.m. The tooling that was a configuration file became a diagram.
Third stage: real rolling deploy gets fragile
With two servers, Kamal offers sequential deploy: first one, then the other. Pretty in description. The problem lives in the error case.
Imagine the deploy on the first server succeeds, the second hangs midway. Kamal has no consolidated view of the state of the two servers after the error. You're left with a new version running on one side, old version running on the other, and no centralized system to reconcile the divergence. Reconciliation becomes manual intervention: you open both servers, decide which version stays, do rollback or advance what failed.
For a three-person team, intervening manually once a quarter is tolerable. For a team that ships four deploys a week across three different applications, manual divergence becomes the main job of one of the three people. That was exactly the trigger that motivated orchestrators being born in the 2010s.
Fourth stage: encryption and routing between services
Sooner or later the application grows and gains a second service — maybe a queue worker, maybe a separate image processing service, maybe an auxiliary API consumed by the main frontend. These services need to talk to each other, ideally with encrypted traffic and control over who can call whom.
Kamal has no opinion on this. It's your job to set up — usually with external proxy, manually issued certificates, hand-written firewall rules. On one server it was trivial (everyone is localhost). On three servers running six services, it becomes a one-week side project.
The router embedded in Kamal (kamal-proxy) elegantly solves the inbound HTTP traffic part — TLS termination, atomic switch between versions, headers under control. But traffic between services, on a private network, is your problem.
The necessary leap
Looking at the four points above together, it's clear that to cover them you need, in practice:
- A replicated control plane between multiple servers — so the fall of one doesn't take down the deploy capability
- Automatic election of the coordinating server, without human intervention
- Integrated balancing and health check, without depending on the provider's external balancer
- Consolidated state of services, with automatic reconciliation when something diverges
- Integrated router with automatic certificates
- Encryption between services embedded, without setting up another product
Many teams' temptation, on reaching that list, is to fall into the planetary orchestrator and end the matter. Then they install the colossus, write three hundred lines of manifest to bring up what was forty lines in Kamal, hire a senior-paid specialist just to keep that breathing, and discover they swapped the right problem for a bigger problem.
The reasonable answer is a tool that offers the six items above without asking for a dedicated team. That's exactly where HeroCtl lives.
HeroCtl as Kamal with a real cluster
The conceptual simplicity is the same. You describe the service in a short configuration file — around fifty lines for a complete application with routing rules, secrets, and automatic certificate. Submit the service through the command-line client or the embedded web panel. The cluster decides where to run.
The difference is under the hood. Instead of transactional SSH, HeroCtl maintains a control plane replicated between three servers. Those three talk to each other all the time to keep a consolidated view of everything that's running, on which server, in which version, in which health. When the coordinating server falls, in around seven seconds one of the other two takes over — without human intervention, without a pager alert for anyone. The application's containers that were running on the fallen server are reallocated on the survivors.
The daily operation looks exactly like Kamal: you change the image version, submit, the cluster orchestrates the substitution. The difference appears in the bad cases. If the partial deploy fails midway, reconciliation is automatic — the desired state is recorded in the control plane, and the agents on each server converge to it without you opening a terminal. If a server falls during deploy, the others take over its work. If the port that was going to be used is stuck by a zombie container, the cluster waits or redirects — it doesn't fail the deploy.
And the embedded tooling covers the other items on the list: router with automatic Let's Encrypt certificates, encryption between services without setting up anything externally, web panel to see what's running, centralized metrics and logs without an external stack.
The installation is the same gesture Kamal asks for: Linux server with Docker, single setup command. There's no new infrastructure requirement. There's no external database. There's no managed cloud service. The cluster lives on the servers you already have.
Side by side, no flourishes
| Criterion | Kamal | HeroCtl |
|---|---|---|
| Philosophy | Minimalist SSH deploy, no external state | Cluster with replicated control plane |
| Ideal server range | 1 (excellent), 2-3 (with external scaffolding) | 3 to 500 |
| Real high availability | Not native — requires external balancer | Embedded; automatic election in ~7s |
| Web panel | No | Embedded |
| Automatic certificates | Yes, via embedded router | Yes, via embedded router |
| Encryption between services | Not native | Embedded |
| Persistent metrics | Not native | Internal job |
| Centralized logs | Not native — collect externally | Embedded single writer |
| Automatic reconciliation after partial failure | No — requires manual intervention | Yes |
| Commercial model | Open, no associated paid product | Permanently free plan + paid Business/Enterprise |
Kamal's column isn't punishment — it's honesty. The product was designed for a specific use case and meets it elegantly. When your use case goes beyond what it covers, the right answer is to swap tools, not force Kamal to become what it never wanted to be.
Stay on Kamal if...
The virtue of a specialized tool is admitting where it wins. Four profiles where we recommend Kamal without hesitation, even if HeroCtl exists.
You run a single server, no formal SLA pressure. The conceptual cost of a cluster — understanding quorum, replicated control plane, election — is unjustified for a single server. Kamal gives you ninety-nine percent of what matters with five percent of the concept.
You're a one- to three-person team with strong Rails culture and no time available to learn new tooling. Kamal is, in practice, a natural extension of bin/rails. Adopting it costs an afternoon. Adopting anything else costs a week — and that week, in your context, is worth more than the operation improvement that would come later.
Your applications are internal or staging, where five minutes of monthly downtime cause no real damage. Kamal's operation is so direct that it remains the best choice even for large teams that have a set of secondary applications with high failure tolerance.
You're DHH. With sincere respect. The thesis that orchestration is overkill was defended by someone who operates public products with millions of users and proves daily that you can do without a cluster, with just well-configured dedicated servers. If you're on a team where this philosophy is part of the identity, Kamal isn't just a tool — it's a statement. There's no technical reason to abandon it.
Migrating from Kamal to HeroCtl when it makes sense
For those who reached the moment where the pain described above became routine, migration is lighter than it seems, because most of the work Kamal already did remains valid.
The Dockerfile that Kamal uses to package the application serves with no change. HeroCtl consumes the same image that Kamal pushes to the registry. There's no need to adapt ENTRYPOINT, expected environment variables, exposed ports — the container's contract is preserved.
Environment variables migrate with the same keys. HeroCtl has its own secrets system, but the variable names the application consumes remain the same. You import the contents of the .env that was in deploy.yml directly into the cluster's internal vault, and the application doesn't notice the swap.
Named volumes are kept, because Docker is Docker. If you had a volume called app_storage in Kamal to persist uploads, in HeroCtl it continues to be called app_storage and lives in the same place on the server. The difference is that the cluster knows where it is and respects that pinning when deciding where to run the application.
The deploy.yml doesn't convert one-to-one to HeroCtl's job spec, but the conceptual mapping is almost mechanical:
- Kamal's
serversblock becomes the notion of cluster with N nodes in HeroCtl. You don't list IPs in the application file — the cluster already knows itself. - Kamal's
proxyblock becomes the integrated routing configuration. Instead of describing the proxy explicitly, you describe the domain name and the rules; the embedded router applies. - The
accessoriesblock (Postgres, Redis, and similar alongside the application) becomes auxiliary jobs in the same cluster, managed like any other service. - The
envblock becomes HeroCtl's secrets system, with the same keys.
Honest estimate: one to three hours for a medium-complexity application, counting time reading documentation, converting the file, first validation deploy. Applications with many accessories or unusual routing rules can reach half a day. Above that, write to us — we have an experimental converter that covers the common cases.
Questions we get
Does HeroCtl have the equivalent of kamal accessories?
Yes. In HeroCtl you describe Postgres, Redis, or any other auxiliary service as an ordinary job, managed by the same cluster that runs the main application. The practical difference is that those auxiliary services get the same treatment as the application: high availability when it makes sense, automatic reconciliation, certificates if they're publicly exposed, metrics and logs in the central panel. You don't operate a separate set of "things on the side".
What if I use Kamal for a small app and HeroCtl for a larger one, can I? Yes. Both worlds coexist without conflict. We even recommend this model during migration: you keep what works on Kamal exactly as it is, and use HeroCtl for the applications where the real cluster matters. The only care is not to run Kamal and HeroCtl on the same server — they compete for space in Docker and ports, and the confusion isn't worth the operational gain. Different servers, different worlds.
Does HeroCtl run on any Linux VPS like Kamal? Yes. The premise is the same: Linux server with Docker. Major cloud provider, smaller provider, dedicated server, virtual machine in the office — wherever Kamal works, HeroCtl works. There's no managed private network requirement, no external provider balancer requirement, no special filesystem requirement. The minimum condition is three servers that can see each other on the network and have Docker installed.
How much more does it consume than Kamal? The control plane occupies between 200 and 400 MB per server. On three servers of modest configuration, that's less than five percent of total available memory. Compared to Kamal — which consumes zero because it has no resident process — it's more. Compared to the colossus, whose managed version starts at around 700 MB per master node before any application comes up, it's half. The right question isn't "how much more", it's "how much does that give me in return". For our public demonstration cluster, which runs four servers, sixteen containers, and five sites with five vCPUs and ten total gigabytes, the answer is: real high availability, without needing to double the hardware.
And kamal-proxy, which is the integrated router underneath?
HeroCtl has its own integrated router, with atomic switch between versions, automatic TLS termination via Let's Encrypt, and routing by domain name. The concept is the same as kamal-proxy. The difference is that HeroCtl's router knows the cluster state — it knows when an entire server has fallen, when a container is in rolling, when a new version hasn't yet passed the health check. An isolated router does routing; a router integrated with the control plane does informed routing.
Do I need to learn a new language to use HeroCtl?
No. The configuration file is plain text, similar to Kamal's deploy.yml in structure and size. The command-line commands follow the verb-noun pattern that anyone who has run Docker knows. The web panel covers ninety percent of operations without you needing to open a terminal. There's no templating language, there's no parallel package system, there's no ceremonial manifest. The internal rule is the same as Kamal's: the configuration has to fit on one screen.
When isn't HeroCtl the right answer? When you operate a single server with no SLA pressure, Kamal is better — and we said this above. When you operate hundreds of thousands of machines at planetary scale, the colossus is better — and we said this in another post. When your compliance officer needs to point to an existing certificate with a product name on the list, today the answer is the colossus or a mature commercial orchestrator. In these three conditions, HeroCtl doesn't compete well and we don't try to force it. For the range between three and five hundred servers, with real availability pressure and scarce time to set up a stack, that's where the tool was designed to shine.
Closing
The right question isn't Kamal or HeroCtl. It's: has your second serious customer already shown up? If not yet, stay on Kamal and be happy — anything different from that is distraction. If already, and the badly slept night with the fallen server has already happened at least once, the answer starts to lean.
The test path is a single command:
curl -sSL get.heroctl.com/install.sh | sh
Run on three servers, bring up the application, kill one of them by force, watch the cluster reallocate. If the relief feeling is proportional to the pain you had in the last incident, it's settled. If it's not, go back to Kamal without guilt — it means your moment hasn't come yet, and respecting that is as important as adopting the right tool when the moment comes.
The longer story of why we built this, and the honest reading of the three paths that existed before, is in Why we created HeroCtl.