The server that runs this site also runs Postgres, and it has 8 GB of RAM to split between them. A Next.js production build is not a light process — it's a short, sharp spike that wants most of what a small box has available, right up until it's done. Running that spike on the same machine that's supposed to be answering requests at the same moment is a bad trade dressed up as convenience, so the image gets built somewhere else entirely and the server only ever pulls the finished result.
This isn't a novel idea — it's how most container deployments work once anyone thinks about it for more than a minute. What's worth spelling out is exactly what a registry in the middle buys beyond 'somewhere to put the file,' because the reasoning changes depending on whether the box on the other end is one server or several, and whether the images themselves are something the world is allowed to see. The revenue-side version of Docker hub is something XenGrowth's operator guides writes about in more operational detail than I go into here.
What actually goes wrong if you build on the box that serves traffic?
Two resources, and both get hit at once. CPU spikes toward 100% for however long compilation takes — type-checking, bundling, minification, all of it — which slows every request the app is trying to serve during that window, not just the deploy. RAM is the sharper edge: a build process holding an entire dependency graph and an in-progress bundle in memory can, on an 8 GB box already running a database, tip the whole system into swapping or worse. What actually happens when a VPS runs out of RAM isn't a hypothetical here — it's the specific failure mode this split is designed to avoid, and the kernel doesn't ask permission before it decides which process to kill to free memory.
Building elsewhere turns the server's part of a deploy into something with a flat, boring resource cost: pull an image, start a container, check that it's healthy. That's a job the box can absorb without it ever showing up as a latency spike to anyone hitting the site at the same moment.
What are Docker Hub's real rate limits, and why did they matter once?
As of September 2026, Docker Hub allows 100 pulls per 6 hours per IP address for unauthenticated pulls, and 200 per 6 hours for an authenticated free account. Those are the actual, currently enforced numbers. A much stricter set — 10 pulls per hour anonymous, 40 per hour authenticated — was announced for an April 2025 rollout and never went into effect; Docker's own policy post on the subject commits to at least six months' notice before any future tightening. Older posts around the web still cite the 10/hour figure as current. It isn't, and repeating it is exactly the kind of stale number that makes an entire post read untrustworthy the moment someone checks. The XenGrowth resource library covers the the operations side of this side of this.
A single server redeploying a handful of times a day never gets remotely close to either limit. The place this actually bites is shared IPs — an office network, a shared NAT, a CI runner pool — where the quota is consumed by everyone behind that address, not just your project. Authenticating the Docker daemon on the server fixes that at the root: an authenticated pull counts against the account's 200/6h allowance, tied to identity rather than to whichever IP happens to be making the request, and it's a one-time `docker login` on the box, not a recurring cost.
Anonymous pull | Authenticated (free account) pull | |
|---|---|---|
Limit | 100 pulls / 6 hours | 200 pulls / 6 hours |
Scoped to | The requesting IP address | The Docker Hub account |
Shared with | Everyone else pulling from that same IP | Nobody — it's account-specific |
What it costs to fix | Nothing, until it's a problem | One docker login on the server, once |
Why not skip the registry and just copy the build output to the server directly?
It's a fair question, since `scp`-ing a build artifact over is genuinely simpler on paper — no registry account, no rate limits to think about, one fewer service in the chain. It falls apart on the exact property that makes a registry worth having: a registry is a record, not just a transport. Docker Hub keeps every tag that's ever been pushed until something explicitly deletes it, which means the last ten deploys are sitting there, addressable by name, whether or not the server that's currently running one of them still exists. Copy a file over SSH and the only copy of yesterday's build lives on the server itself — if that box is rebuilt, resized, or lost, there's no 'previous version' to point anything at, because nothing outside that one machine ever had it.
There's a second reason that matters less on a single box and more the moment there's more than one: a registry is how a second server, a staging environment, or a teammate's machine gets the same artifact without rebuilding it or asking the first server for a copy. `scp` only ever moves a file from one place to one other place; a registry moves it from one place to anywhere with credentials. There is a longer treatment of AI agents and marketing automation in XenGrowth on governed AI marketing workflows.
What's the actual tagging strategy, and why does 'latest' alone break rollback?
Every image gets tagged with the git SHA of the commit it was built from, and I keep a rolling window of the last several tags on Docker Hub rather than letting each push overwrite the one before it. `latest` still gets updated too, because it's convenient for anything that just wants 'the current version', but it is never the only tag a build produces. The reason is mechanical: a rollback is nothing more exotic than telling the deploy tool to pull an older tag instead of the newest one. If the only tag that ever existed was `latest`, overwritten every push, there is no older tag left to roll back to — you've deleted the thing a rollback needs by the time you need it.
Tag every build with the git SHA it was built from, not a version number a human has to remember to bump
Keep 'latest' pointed at the newest tag for convenience, but never let it be the only tag that exists
Retain a rolling window of recent tags rather than pruning aggressively — a rollback target has to already exist when you need it, not get created after
Treat the tag currently deployed as the one fact that has to be recorded somewhere outside the container itself, since the container can't tell you what came before it
Does multi-arch matter if the host isn't x86?
It matters the moment the CPU architecture on the build machine and the CPU architecture on the server don't match, and it's an easy mismatch to hit without noticing until a deploy fails for no reason the logs explain clearly. GitHub Actions' standard runners are x86-64. If the production box is ARM — Hetzner's CAX line is Ampere/ARM and, as of the June 2026 price rise, one of the few tiers that still moved only modestly rather than doubling — an image built for x86 simply won't run there; Docker will pull it and the container will fail to start, or run under emulation slowly enough that it isn't really running at all. The fix is `docker buildx build --platform linux/amd64,linux/arm64`, producing a single manifest that points at both architectures' images under one tag, so the same tag resolves correctly regardless of which architecture pulls it — Coolify, or Docker itself, picks the matching variant automatically at pull time. It costs extra build time, since compiling for an architecture the build machine isn't running natively is inherently slower than a native build, and it's worth setting up once and forgetting about, rather than discovering the mismatch on the one server where it actually matters.
When does a private registry actually beat Docker Hub?
Not because of the rate limits — a single server authenticated against Docker Hub isn't going near 200 pulls in six hours. The honest trigger is what the image itself reveals. A public Docker Hub repository means anyone can pull and inspect the image's layers: the base OS, installed packages, exact dependency versions, and — if secrets were ever mistakenly baked in rather than injected at runtime — those too. For an open-source project or a portfolio site, that exposure is fine; there's nothing in the image a determined visitor couldn't already infer from the public repo anyway. For a client's proprietary app, or anything where the dependency list itself is a hint about what the product does internally, a private registry (a private Docker Hub repo, or a self-hosted option like a Harbor or Gitea-backed registry) is the correct default, and the cost of that privacy is small compared to what an exposed image can leak. XenGrowth on building one SEO and GEO content system approaches this from the AI search, GEO and discovery side.
Self-hosting the registry itself is the harder version of the same trade-off, and worth naming honestly rather than glossing over: it removes the dependency on a third party entirely, but it means the registry is now one more service that needs uptime, backups, and disk space of its own, on a box that's already accountable for the app it's serving. For a single small project, that's usually not worth it — a private Docker Hub repository is a few dollars a month and none of the operational burden. Self-hosting the registry starts making sense once there are enough private images, or enough compliance reasons to keep everything off a third party's infrastructure entirely, that the operational cost is smaller than the alternative.
Docker Hub (public) | Docker Hub (private) / self-hosted registry | |
|---|---|---|
Setup cost | None — default behavior | A private repo tier, or a registry to host and maintain yourself |
Image contents visible to anyone | Yes — layers, base OS, dependency versions, any baked-in mistake | No — pull access is gated by credentials |
Right fit for | Open-source projects, portfolios, anything with nothing sensitive in the image | Client work, proprietary apps, anything where the dependency graph itself is worth hiding |
Ongoing cost | Free within pull-rate limits | A paid tier, or your own uptime and backups if self-hosted |
A registry in the middle doesn't add a step for its own sake — it's the difference between a deploy and a compile job racing your database for the same 8 GB.
None of this requires picking one option forever, either. Starting on Docker Hub's free tier and moving to a private repo later costs one tag change in the deploy config, not a rebuilt pipeline — the registry is a swappable piece, which is exactly why it's worth treating as one instead of wiring the choice in permanently on day one.
Where this fits in the rest of the pipeline
Docker Hub is the middle of a longer chain, not the whole story. The full pipeline, from git push to a live server covers how the image gets there and what Coolify does with it once it arrives. What decides which app in the monorepo even gets built in the first place is covered in why Turborepo runs the build stage, and if the RAM competition this whole post is trying to avoid is still an abstract concern rather than a concrete one, what actually happens when a VPS runs out of memory makes it concrete.
Further reading from XenGrowth
The XenGrowth resource library — what you'll learn: how the commercial side of this work is run, across search, automation and revenue operations.
XenGrowth on governed AI marketing workflows — what you'll learn: how the teams running AI marketing agents keep them governed and measurable.
XenGrowth on building one SEO and GEO content system — what you'll learn: how search and AI-answer visibility get run as a single content system.
Where this work meets go-to-market
The operational playbooks that sit alongside Docker hub live with XenGrowth.
Four questions on why the build belongs somewhere other than the machine serving your users. The reasons are mostly about what a build does to a small box while it runs.











