You don't need Harbor: a tiny Zot pull-through cache for faster Coolify redeploys
The most annoying moment in self-hosting is a redeploy that hangs on docker pull — or worse, dies with "you have reached your pull rate limit" — right when you're trying to ship a fix. On a single box that pulls the same base images over and over from Docker Hub, a small pull-through cache fixes both problems. You don't need Harbor and its pile of containers to get it. You need one Go binary.
The problem, concretely
Docker Hub's anonymous pull limit is 100 pulls per six hours, counted per IP. A free authenticated account gets 200. That sounds like plenty until your VPS is redeploying repeatedly, or shares an IP range, or you're iterating on a Dockerfile. And even under the limit, every cold pull crosses the WAN to fetch layers you already had last week. Both the rate limit and the latency come from the same root cause: you keep asking Docker Hub for images you've already downloaded.
Zot: a pull-through cache in one binary
Zot is a CNCF project — an OCI-native registry that ships as a single Go binary with no external dependencies (no Postgres, no Redis). Point it at Docker Hub as an on-demand mirror and the first pull of an image fetches and stores it locally; every redeploy after that pulls from your own box at LAN speed and never touches Docker Hub's counter. A minimal config is genuinely small:
{
"distSpecVersion": "1.1.1",
"storage": { "rootDirectory": "/data/zot", "gc": true },
"http": { "address": "0.0.0.0", "port": "8080" },
"extensions": {
"sync": {
"enable": true,
"registries": [
{ "urls": ["https://registry-1.docker.io"], "onDemand": true, "tlsVerify": true }
]
}
}
}
Run it as a container, then consume it by prefixing your images with the Zot host:
# was: image: nginx:1.27
image: your-vps:8080/docker.io/library/nginx:1.27
One honest correction to a lot of tutorials: the reliable way to consume Zot's on-demand cache is this explicit prefix (or a containerd/podman rewrite rule). The classic Docker daemon registry-mirrors array pointing at Zot is finicky and I wouldn't assert it works without testing it on your setup first.
Be fair: you might not even need Zot
I want to steel-man the alternatives, because "install my favourite tool" is bad engineering advice. Plain registry:2 (CNCF Distribution) is also a single Go binary and also does pull-through caching, and it's the more battle-worn default. For a single Docker Hub upstream, Zot and registry:2 are comparable in footprint — so don't believe anyone (including me) who tells you Zot is dramatically "lighter." Zot's real edges are being OCI-1.1-native, content dedup across images that share base layers, online garbage collection, multiple upstream mirrors in one service, and an optional UI. So the honest framing is: Zot is the more modern option than registry:2, and far lighter than Harbor — which, with its Postgres and Redis and half-dozen containers, is genuine overkill for one operator.
The right registry for one box isn't the most powerful one. It's the one that solves the rate-limit-and-cold-pull problem without becoming a second system you have to babysit.
The downsides, stated plainly
A cache is not free, and pretending otherwise is how you get bitten:
- It's another always-on container to run, patch, and watch. If you turn on the UI and CVE-scan extensions, its RAM climbs noticeably — leave those off on a small VPS.
- It can go stale. With on-demand caching, a mutable tag like
:latestwon't refresh once cached unless you configure periodic sync or pull by digest. Pin digests for things that matter. - You now depend on it for deploys. If Zot is down or its disk fills, your redeploys break. Set storage GC/retention so the cache doesn't grow forever, and remember you've added a link to your deploy chain.
Is it worth it for one blog?
If you redeploy a lot, or you've ever hit the Docker Hub limit at the wrong moment, yes — the first time a deploy pulls in a second instead of thirty, you'll be glad. If you deploy once a week and never see the rate limit, honestly, skip it; it's a container earning its keep only under repetition. That's the whole calculus for a one-box operator: a cache pays off exactly in proportion to how often you'd otherwise be re-downloading the same bytes. Measure your own redeploy pull time before and after — if the "before" doesn't annoy you, you don't have this problem yet.