Every mainstream self-hosted deploy tool assumes the same middle step: build an image, push it somewhere, then tell a server to pull it back down. Haloy, a small open-source deploy tool that hit beta in 2026, skips that step entirely. Its CLI builds your Docker image on your own machine, asks the haloyd daemon on your target server which layers it already has cached, uploads only the ones it doesn't, and hands off to haloy-proxy for a health-checked, zero-downtime traffic swap — no Docker Hub, no GHCR, no registry credentials anywhere in the loop. Kamal and Dokku, the two tools it's most directly compared against, both still route through a registry the moment you're not deploying to a single box by hand. Here's exactly what removing that hop changes, and where it doesn't.
Three tools, three answers to "where does the image live"
The three tools solve the same problem — get a container running on a server you don't want to hand-operate — with genuinely different answers to where the build happens and whether a registry is required to get the result onto the target machine.
| Where the build happens | Registry required? | What changes at multi-server scale | |
|---|---|---|---|
| Kamal | Your machine or CI | Always. Kamal's own deploy flow builds an image, pushes it to Docker Hub/GHCR/ECR/a private registry, then SSHes into each host and tells it to docker pull | No change — every host still pulls from the same registry, one at a time by default (rolling at the fleet level) |
| Dokku | On the target server itself, triggered by git push | No, for one server. Dokku is a Heroku-style buildpack/Dockerfile builder that runs on the box you're deploying to — there's nothing to push anywhere | Yes, past one server. Dokku's own docs for its k3s scheduler plugin state the requirement directly: multi-node clustering needs a configured registry so new nodes can pull the images the primary server built |
| Haloy | Your machine (CLI shells out to your local Docker) | No, at any scale documented so far. haloyd reports which layers it already has; the CLI uploads the diff directly over HTTPS | Each target server still gets a direct upload from the CLI — there's no fleet-wide pull step to reintroduce a registry for |
The detail worth sitting with is the Dokku row, because it's the one most write-ups get lazy about. Dokku isn't "registry-dependent" in the way Kamal is — a solo Dokku box has never needed a registry, because there's no separate build machine to hand an image to. It only becomes registry-dependent the moment it stops being a single box, which is exactly the scaling transition a lot of teams hit without planning for it. Kamal doesn't get that grace period: even a single-server Kamal deploy pushes to and pulls from a registry, because Kamal's SSH-based model was designed around "any host, any time" rather than "the one box currently running the build."
Haloy is the only one of the three where neither condition — solo box or multi-server — reintroduces a registry, because the CLI talks to each target's haloyd directly instead of routing through a shared pull source.
It's worth being upfront about how young that claim is. Haloy is a small project — around 150 GitHub stars, still shipping frequent v0.1.0-beta.* releases as of mid-2026 — built and maintained by a small team rather than the Basecamp-backed Kamal or the decade-plus-old Dokku. The architecture holds up under the docs and source available today, but it hasn't had years of production traffic to sand off the edge cases Kamal and Dokku have already hit and fixed. Treat the comparison as "what the design gets right," not "which tool to bet a production fleet on today."
What the CLI and the daemon actually do
The deploy sequence, per Haloy's own docs and repo:
- You write a
haloy.yamldescribing the app — a Dockerfile path (or a pre-built image), the server(s) to target, health check settings, and domain/TLS config. - Running
haloy deploybuilds the image locally using the Docker daemon already on your machine — this is a hard prerequisite; Haloy has no remote-build mode of its own. - The CLI talks to
haloydon the target server and compares the layers in the freshly built image against whathaloydalready has cached from the previous deploy. Only the layers that changed get uploaded, over HTTPS, directly to the daemon — no intermediate storage, no registry API, no separate push-then-pull round trip. haloydassembles the image from cached-plus-uploaded layers, starts the new container alongside the running one, and waits for it to pass its configured health check.- Once healthy,
haloydpushes an updated routing snapshot tohaloy-proxy, which flips traffic to the new container and terminates the old one.haloy-proxyruns as a separate process specifically so it keeps serving the last-known-good routes even ifhaloyditself is mid-restart or briefly down. - Rollback is "deploy the previous image again" — Haloy keeps enough local/server-side state to make that a normal deploy rather than a special recovery procedure.
Two things are worth being precise about. First, Haloy still supports a conventional registry-backed workflow for teams whose CI already builds the image elsewhere — the layer-diff path is the default, not the only mode. Second, "zero-downtime" here is doing the same job kamal-proxy does for Kamal: boot the new container, health-check it, only then cut traffic, and keep the old one running as a fallback until the new one proves itself. That part isn't Haloy's differentiator — Kamal's proxy layer already does health-checked, no-downtime swaps well. What's different is entirely upstream of the swap: how the bits get from your laptop to the server in the first place.
The layer-diff step is also where most of the practical win sits day to day, not just on the first deploy. A typical app image is mostly base-OS and runtime layers — Node, Python, Ruby, whatever the base image pulls in — that don't change between deploys, plus a thin top layer or two holding your actual code and dependencies. On a registry-based tool, every deploy still pushes and pulls the full manifest even though only that top layer changed, because the registry protocol doesn't know or care that the base layers are identical to what's already sitting on the target. Haloy's docs describe the same shape of savings from the other direction: because haloyd already knows what it's holding, "subsequent deploys typically transfer a fraction of the full image size" — the diff, not the whole image, every time after the first.
The failure mode this actually removes
That upstream difference matters because a registry isn't just extra latency — it's a dependency that can go down or throttle you independently of anything you control. Docker's own status history logged a full Hub-and-Registry service disruption on December 5, 2025, lasting roughly 20 minutes — short, but long enough to stall every pull-dependent deploy pipeline mid-flight during that window, whether or not the app itself had changed. Outside discrete incidents, Docker Hub's anonymous and free-tier pull-rate limits are a standing, not occasional, failure mode: a shared NAT'd IP or a burst of CI runners hitting the same 100-pulls-per-6-hours ceiling returns 429 toomanyrequests and blocks the deploy, with no code change and no registry outage involved at all.
Neither of those failure modes touches Haloy's deploy path, because there's no registry in it to be down or rate-limited. That's the actual claim worth making — not "Haloy deploys are safer" (Kamal's health-checked swap already covers that), but "Haloy has one fewer external dependency that can independently fail the deploy, and one fewer network hop on every deploy after the first." For a solo developer or small team on a handful of Haloy-managed boxes, that's a real, if narrow, reliability and latency win: incremental deploys transfer only what changed and never touch a service outside your own machine and your own server.
Does a Cluster-API platform have anything to gain from this?
The honest answer is: not the mechanism itself, but the property it's chasing, yes.
Kubernetes' image pull path is shaped differently than either Kamal's or Haloy's. A kubelet doesn't accept a client-pushed layer diff over HTTPS from whoever ran deploy — it pulls images through the container runtime's CRI interface, which speaks the OCI Distribution API to a registry. There's no kubelet-native equivalent of "here's a build-host uploading layers directly to you," because the whole pull model assumes a registry sits between the build and every node that might schedule the workload — not just one target server. Bolting Haloy's direct-upload model onto a Cluster-API-provisioned fleet would mean replacing a piece of Kubernetes' own scheduling contract, not just swapping a deploy CLI.
What a Cluster-API-based platform can take from the comparison is the underlying design instinct: don't let an external registry sit in the critical path of every deploy. That's exactly why running your own in-cluster registry — Harbor, or something leaner like Zot — instead of pulling straight from Docker Hub or GHCR at deploy time isn't just a cost optimization; it's the same "external registry independently failing your deploy" risk Haloy eliminates by removing the registry altogether, addressed by keeping the registry, but keeping it inside the network boundary where an upstream outage or someone else's rate limit can't touch it. The open question worth actually pricing out is whether it's worth going further and building registry-side layer dedup so a node's incremental pull is proportional to what changed — mirroring, on the pull side, what Haloy's client already gets for free on the push side.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



