Your CI pulled node:20-alpine 247 times in the last six hours and Docker Hub just returned 429 Too Many Requests. The fix Docker sells is $15 per seat per month. The fix you can run on the box you already pay for is a Harbor cache that turns those 247 pulls into one.
Both answers cost money. Only one of them stops the next burst from failing the same way. Here is what each option actually costs, what breaks under burst load, and when the six-month promise Docker made in February 2025 is — and isn't — a substitute for running your own mirror.
The upfront answer for a team deciding this week:
| Option | What you pay | Pull limit | What survives a 20-job burst | Marginal cost if you already run a Hetzner fleet |
|---|---|---|---|---|
| Docker Hub free (anonymous) | $0 | 100 pulls / 6 hr per IP | Fails — shared GitHub Actions IP exhausts in minutes | $0 |
| Docker Hub free (authenticated) | $0 | 200 pulls / 6 hr per account | Fails under ~15 concurrent jobs pulling base images | $0 |
| Docker Hub Pro | $9 /user /mo ($108/yr) | Unlimited | Survives — but per-user billing | $540/yr for 5 seats |
| Docker Hub Team | $15 /user /mo ($180/yr) | Unlimited | Survives — best per-seat fix for small teams | $900/yr for 5 seats, $9,000/yr for 50 |
| Docker Hub Business | $24 /user /mo ($288/yr) | Unlimited | Survives — SSO, audit, 50k/day old cap gone | $14,400/yr for 50 seats |
| Harbor pull-through cache on existing fleet | $0 license (CNCF, Apache 2.0) + disk | Unlimited (your hardware is the limit) | Survives — first pull caches, next 199 never hit Docker Hub | ~$0 — runs on nodes you already run for apps |
If you already operate a Cluster API fleet on Hetzner, Harbor's marginal cost is disk and the hour you spend pointing your daemon.json at it. If you don't run your own fleet, paying Docker per seat is the honest, cheaper answer for a team under ~15 people.
The rest of this post shows where those numbers come from, what "100 pulls per 6 hours" actually means when runners share an IP, and how Harbor's proxy cache changes the math.
What Docker Actually Changed — and Then Unchanged
November 2024, Docker announced its biggest Hub repackaging in years. The headline numbers were simple:
- Pro: $5 → $9 per month (+80%), annual price $60 → $108.
- Team: $9 → $15 per user per month (+67%), annual $108 → $180. Business stayed at $24.
- Personal stayed free, but lost Build Cloud minutes and was capped at one private repo with 2 GB.
- Alongside the price moves, Docker announced consumption-based pricing: metered pulls and storage-based billing for Hub.
That last bullet is the one that generated the most migration tickets — and the one Docker walked back.
By February 2025, under sustained pushback, Docker published a reversal:
- "No More Pull Count Limits or Consumption Charges" — pull consumption charges cancelled entirely. All paid subscribers get unlimited pulls with no consumption charges or rate limits.
- Storage pricing delayed indefinitely. Docker said it would focus on storage-management tooling first, and if storage billing ever returns, it will give at least six months' notice.
- Free-tier limits were tweaked rather than removed: anonymous pulls stayed in the 100-per-6-hour band, authenticated free moved to 200 per 6 hours (and later, for a period, 100 per hour in some docs — the steady-state enforcement most operators hit is 100 anonymous / 200 authenticated per 6 hours per IPv4 or /64).
The result is a two-tier reality that matters for the decision:
- If you pay — any paid tier, Pro/Team/Business — you have no pull limit to think about. The $9 or $15 is the whole answer.
- If you don't pay — you sit on a 100 or 200 per 6 hours budget shared across every anonymous pull from your NAT IP or every pull on a shared CI runner's IP. And you have Docker's written promise that the next breaking change comes with six months' warning — not that it won't come.
That promise is the hinge of the whole Harbor-vs-pay question. We'll come back to whether it's load-bearing.
What "100 Pulls per 6 Hours" Actually Costs You in a Burst
The number sounds generous until you count what a pull is and who you share an IP with.
One docker pull counts as one manifest GET. Layers don't count separately — but every distinct image reference does. A typical CI matrix that builds from node:20-alpine, runs tests in postgres:16, and lints in golang:1.22 burns three pulls before a single line of your code is fetched. Multiply by jobs:
- 10 parallel GitHub Actions jobs × 3 base images = 30 manifest GETs in one push.
- Two pipelines for the same commit (Woodpecker's
push+pull_requesttriggers, or a PR push that re-runs a matrix) doubles it to 60. - An hourly Renovate or digest-pinning job that resolves every base image even on a full cache hit adds a steady background burn — one real report showed that a nightly
warm-imagesjob alone tripped 200 per 6 hr during PR bursts.
On GitHub-hosted runners, the problem is worse than per-repo math suggests. Runners share egress IPs. Anonymous pulls from unrelated repos on the same provider pool count against the same 100-per-IP window. Issue trackers across 2024–2026 show the same pattern on repeat:
toomanyrequests: You have reached your unauthenticated pull rate limitongolang/nodemanifest GETs during digest resolution — hard-fails in ~9 seconds, no retry, entire downstream matrix skipped as aneeds:prerequisite.- Flakes that trigger
429 Too Many Requestsonregistry-1.docker.iowithRegistry credentials or Docker config not provided. Guest mode enabled. - A ClickHouse-scale matrix with ~219 concurrent jobs where anonymous, uncached pulls across the fleet timed out and the logs were gone before anyone could tell rate-limit from slow pull.
Authentication (a free Docker ID) doubles the budget to 200 per 6 hours, but doesn't fix sharing. A single celestiaorg-style shared service account across multiple caller repos trips the rolling limit when several repos build at once. Adding retry + exponential backoff around the pull — as some mitigation guides suggest — masks the symptom; the next concurrent push re-hits the same wall.
This is the failure mode Harbor fixes structurally rather than cosmetically. It doesn't raise the limit. It removes Hub from the hot path.
Harbor as a Pull-Through Cache: One Pull, Then Zero
Harbor is a CNCF Graduated project (Apache 2.0) that most teams know as "the self-hosted registry." Its less-understood feature for this problem is proxy cache, shipping since Harbor 2.1: a Harbor project configured as a pull-through mirror for an upstream registry.
How it works:
- You create a Harbor project with Proxy Cache enabled, pointing at
https://registry-1.docker.io(or GHCR, ECR, Quay — any OCI-compatible upstream). - You point your runtimes at Harbor instead of Docker Hub:
harbor.yourdomain.com/dockerhub-proxy/library/node:20-alpineinstead ofdocker.io/library/node:20-alpine, or transparently via/etc/docker/daemon.jsonregistry-mirrors. - On the first pull for a given manifest, Harbor fetches from Hub, serves the client, and caches the layers locally.
- The next 199 pulls for the same digest never leave your network. They hit Harbor's local storage at LAN speed — one report measured cache-served pulls dropping from minutes to milliseconds.
That proxy project can mirror more than Hub. The same Harbor instance can front ECR Public, GHCR, k8s.io, and Quay, consolidating every external pull through one cache you control. ECR's own pull-through cache covers a fixed upstream list; Harbor's covers any OCI upstream, which is why it shows up as the portable answer for a fleet that builds and deploys tenant images behind one Cluster API control plane.
The operational shape matters for a PaaS team: Harbor runs as a Helm chart on the Kubernetes cluster you already operate for tenant workloads. It needs Postgres, Redis, and a PVC for the registry itself (plus Trivy if you want scan-on-push). Minimum viable sizing docs cite 2 vCPU, 4 GB RAM, 40 GB disk — exactly a Hetzner CX22 (€4.35/month) or a slice of an existing node pool.
Line-by-Line Cost: Pay Docker vs. Run Harbor
The question is never "Harbor vs. Hub" in the abstract. It's Harbor on hardware you already run versus a per-seat subscription for a line item that used to be free. Recompute it for three team sizes that bracket most PaaS operators.
If you already run a Hetzner fleet under Cluster API
Your fleet already has nodes, a private network, and a Longhorn or Hetzner Volume PVC class. Harbor colocates with tenant workloads. The marginal bill:
| Component | Cost on existing fleet | Notes |
|---|---|---|
| Harbor license | $0 | CNCF Graduated, Apache 2.0. No per-seat, no per-pull. |
| Compute | ~$0 marginal | Schedules as pods on existing nodes. No new VM at small scale. |
| Storage (registry PVC) | 50–500 GB at Hetzner Volume rate (€0.10/GB/mo) or Longhorn on local NVMe | One node:20-alpine is ~50 MB; even 200 distinct upstream images ≈ 10–30 GB. Backing store, not bandwidth, is the line. |
| Bandwidth | $0 marginal | Harbor's 20 TB per CX22 allotment covers pull-serve traffic; cache hits don't egress to Hub at all. |
| Ops (your time) | ~2–4 hr setup, ~1 hr/month | Helm install, TLS via cert-manager, registry-mirrors rollout. Trivy scans are optional but free. |
Compare that to paying Docker on the same fleet:
| Team size | Docker Pro ($108/yr/seat) | Docker Team ($180/yr/seat) | Docker Business ($288/yr/seat) | Harbor marginal |
|---|---|---|---|---|
| 5 seats | $540/yr | $900/yr | $1,440/yr | ~$0 + disk |
| 20 seats | $2,160/yr | $3,600/yr | $5,760/yr | ~$0 + disk |
| 50 seats | $5,400/yr | $9,000/yr | $14,400/yr | ~$0 + disk |
Even the older Business-at-$300/month flat math (the $3,600/year figure cited in pre-2025 comparisons) was cheaper than per-seat Business for 20+ users — but that flat number is gone. Today's Business is per-user, which is exactly why the cache wins harder as seat count grows.
The break-even is stark: a 5-person team paying Team saves $900/year by self-hosting Harbor — but only if the fleet exists. Which leads to the honest caveat.
If you don't run your own fleet
Harbor is not free. It is a stateful distributed system that wants a Kubernetes cluster, a database, an object store or PVC, backups, and someone on call when its Postgres doesn't come back after a node drain. For a team that currently deploys on Render, Fly.io, or Vercel with no owned nodes, the bill looks different:
| Component | Standalone cost (new infra for Harbor alone) |
|---|---|
| Smallest viable Kubernetes (3× CX22 or equivalent) | |
| Managed Postgres/Volume for Harbor | +$5–15/month |
| Your on-call time | unbounded — Harbor is now your registry's SLA |
At $900/year for a 5-seat Team subscription, paying Docker is cheaper than standing up a fleet whose only job is to cache Docker Hub. The self-hosted win is real but conditional: Harbor is the cheap answer when the fleet is already a sunk cost, not when you would build a fleet to justify the cache.
This is the same shape as every other bex thesis: owning the substrate wins when you already own it for everything else. The registry is a rider on the fleet you operate for tenant apps, builds, and private images — not a standalone product.
The Six-Month Notice: Is Docker's Promise Enough?
After the February 2025 reversal, Docker gave two explicit commitments:
- Pull consumption charges are cancelled — paid users are unlimited.
- If storage billing ever returns, it comes with at least six months' notice. Pull limits remain 100/200 per 6 hr for free users; paid users have no limit.
Is that promise a substitute for a mirror? For two distinct audiences, the answer splits:
For a small team that can pay: yes, plausibly. $9 or $15 per seat buys you out of the rate-limit problem with zero ops. The six-month notice covers the scarier unknown — storage billing or a new meter — with time to react. If your burst profile is 5 concurrent jobs and you authenticate, you may never trip 200 per 6 hr in practice, and the subscription is insurance you can expense without running a stateful service.
For a platform that builds for others — a PaaS, a CI provider, a team whose product is "we run 200 builds an hour for tenants" — the promise is necessary but not sufficient. Three reasons:
-
Six months is not zero risk. Docker's November 2024 announcement gave roughly 60 days before the January 2025 pull enforcement was slated to begin (later slipped to February). The "at least six months" language is post-hoc. Trusting it means trusting not just today's docs but the business pressure that produces tomorrow's docs. Docker's pricing page has moved from $5 to $9 and introduced, then retracted, a consumption model in under 15 months. A promise measured in months is not an architecture.
-
Free-tier limits still apply while you wait. Six months' notice for a future storage charge says nothing about the 100/200 per 6 hr limit you hit this afternoon. That limit is not "delayed indefinitely" — it is enforced now, and it is per-IP, which no single team's authentication fully controls on shared runners.
-
Blast radius is inherited. When Docker Hub had a multi-hour outage or a rate-limit enforcement bug, every team without a cache waited in the same queue. A pull-through cache makes your deploys and builds correlated with your own fleet's health — which you can observe and fix — rather than with 1,082 other providers' incident on the same day.
Harbor doesn't eliminate the promise. It makes the promise non-load-bearing. You can still pay Docker if you want the convenience; the cache means a pricing-page change is a negotiation, not an outage.
When Paying Docker Is the Right Call
A good self-hosting post should tell you when not to self-host. Harbor is the wrong choice when:
- You have no fleet and no plan to run one. If you deploy on a managed PaaS, adding a 3-node Kubernetes cluster to cache Hub images is a hobby that invoices you monthly. Pay for Team, authenticate your CI with a service account, and move on.
- Your pull volume is low and authenticated. A 3-person team with 5 concurrent CI jobs that authenticates with a single service account will rarely trip 200 per 6 hr. The per-seat cost is noise next to compute.
- You need Hub's collaboration features more than its pulls. Organizations, private repos, Docker Scout, Build Cloud minutes, and the social graph of
docker pullare Hub features Harbor doesn't replicate. If Hub is your team's package collaboration surface — not just a pull source — the subscription buys more than rate-limit relief.
Harbor is the right call when:
- You already run a Cluster API fleet on Hetzner, OVHcloud, or bare metal for tenant apps. The cache rides on capacity you've already paid for; the marginal cost is disk.
- Your CI is bursty, parallel, and shared-IP. GitHub Actions, Woodpecker, or Concourse with matrix builds and Renovate digest resolution is the textbook Harbor workload.
- Your deploys must not depend on an external registry being up. Air-gapped or sovereignty-sensitive workloads already need a local registry; proxy cache is a free second win.
A practical decision tree:
Do you already operate a Kubernetes fleet for apps/builds?
├─ No → Pay Docker Team/Pro, authenticate CI, set a 429 retry with backoff.
└─ Yes → Is your burst pull rate > ~150 per 6 hr or do you share runner IPs?
├─ No → Authenticated pulls + retry is probably fine; Harbor is optional.
└─ Yes → Deploy Harbor proxy cache on the fleet; point daemon.json / imagePullSecrets at it.Putting It Together on a Bex Fleet
If you run bex — the open-source, Render-compatible PaaS on Cluster API — this is a one-Helm-chart addition to a fleet you already reconcile:
helm repo add harbor https://helm.goharbor.io
helm install harbor harbor/harbor \
--namespace harbor --create-namespace \
--set expose.ingress.hosts.core=harbor.yourdomain.com \
--set persistence.persistentVolumeClaim.registry.size=100Gi
# Create a proxy-cache project targeting https://registry-1.docker.io
# Then point nodes:
# /etc/docker/daemon.json → { "registry-mirrors": ["https://harbor.yourdomain.com"] }Tenant App builds and MachineDeployment rollouts pull through Harbor; the first fetch warms the cache, the next hundred never leave the private network. Vulnerability scanning via Trivy, retention policies, and replication to a second region are the same Harbor project you already use for private tenant images — no second system to operate. The cost delta is a PVC line on a bill you already pay for the node's 20 TB of included bandwidth, not a new vendor meter.
Bottom Line
Docker's 80 and 67 percent price hikes are real, and so is the reversal that cancelled consumption billing for paid users. The two facts together create a clean, un-dramatic market: pay $9–$15 per seat and forget about pulls, or keep pulling free and accept a 100 or 200 per 6 hour budget that burst CI will trip on shared infrastructure.
Harbor adds a third option that only pencils out on the right substrate: a CNCF-graduated, Apache 2.0 cache that collapses your Hub dependency from "every pull" to "first pull" and moves the rest onto disk and bandwidth you already own. On an existing Hetzner fleet under Cluster API, the marginal cost rounds to zero and the reliability win is structural. Without a fleet, the honest math says to pay Docker and authenticate your runners.
The six-month notice Docker gave in February 2025 is a genuine improvement — but a notice period is a heads-up, not an architecture. A cache is the architecture. If you run the fleet, run the cache. If you don't, pay the seat. Either way, stop letting an anonymous pull budget decide whether your deploy succeeds.
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.