A year ago Docker was going to charge you per pull. Then it didn't.
In November 2024 Docker announced consumption-based billing for Docker Hub pulls and storage, with new per-hour rate limits taking effect March 1, 2025. By February 2025 the consumption charges were cancelled outright, storage billing was shelved indefinitely, and the enforcement date was pushed to April 1. The only limits that stayed were the ones from 2020: 100 pulls per 6 hours anonymous, 200 per 6 hours for a free authenticated account. Paid subscribers got unlimited pulls.
If you run a self-hosted PaaS on Kubernetes — especially a Cluster API fleet on Hetzner where every node pulls the same base images on every scale-up and rollout — the question that announcement left behind is sharper than the announcement itself: do you still need a pull-through cache, or is Docker's written promise of "at least six months' notice before any future enforcement" enough to wait?
TL;DR: Wait vs. Mirror
| Fleet shape | Typical steady-state pulls | Risk without mirror (authenticated) | Verdict |
|---|---|---|---|
| Solo dev, 1-3 nodes, ~15 distinct images | < 30 pulls / 6hr window | Near zero — 200/6hr free tier covers you, paid tier eliminates it | Wait. Authenticate pulls, monitor headers, do nothing else |
| Small team, 5-10 nodes, 20-40 images, weekly deploys | 40-120 pulls / 6hr in a rolling-update burst | Low if authenticated free (200/6hr), negligible if paid (unlimited) | Wait + harden. Centralize imagePullSecrets, watch rate-limit headers |
| PaaS fleet, 20-50+ nodes, multi-tenant, dozens of tenant images, autoscaling | 100-300+ pulls in a single burst (one pull per node per image) | Real if anonymous — a 30-node rollout pulling 3 images hits 90 pulls per image layer fetch in seconds — but still low if authenticated paid; the failure mode is the unauthenticated node | Mirror if you must stay anonymous or run bursty scale-out. Otherwise wait with paid auth and a cache on standby |
The punchline: for an authenticated fleet, Docker's current limits are generous. The 50-node horror story that justifies a mirror only hits unauthenticated pulls counted per IP. A self-hosted PaaS doesn't need to pre-build a registry mirror to survive today — it needs authenticated pulls and a six-month runway to build one if Docker ever re-proposes what it just walked back twice.
The rest of this post is the receipt.
What Docker Announced — and Then Un-Announced
Docker's November 2024 plan update looked like a structural change, not a tweak. The headline moves:
- New, tighter pull tiers: unauthenticated 10 pulls/hour per IP and authenticated free 100 pulls/hour.
- Consumption-based charges for pulls beyond the tier.
- Storage-based billing for repositories and layers.
- An enforcement date of March 1, 2025, with new plan entitlements rolling at each customer's next renewal after December 10, 2024.
What actually happened by February 2025:
| Item | Announced (Nov 2024) | Revised (Feb 2025) | Status today |
|---|---|---|---|
| Pull consumption charges | Bill per pull beyond tier | Cancelled outright. All paid subscribers get unlimited pulls, no consumption meter | Not enforced |
| New tighter limits (10/hr anon, 100/hr free) | Enforce March 1 | Not enforced. Old limits kept: 100/6hr anon, 200/6hr free | Old limits remain |
| Storage billing | Charge per GB stored | Delayed indefinitely. Docker says it will build better storage tooling first | Not enforced |
| Future changes | Next renewal date | At least six months' public notice before any future enforcement or storage charges | Written commitment |
Docker's own language, repeated in its February revisiting-post and in The Register's interview with Docker VP of product: paid Docker subscribers have "unlimited image pulls with no consumption-based charges or rate limits," and if storage pricing is ever introduced, "we will give users at least six months' notice."
That is not a delay. It is a reversal on pulls and an indefinite shelve on storage. Docker walked back the threshold, not just the date. It matters because Docker had already done a version of this once before — the 2020 pull limits (100/6hr anon, 200/6hr free) shipped after an extended progressive-enforcement period and only affected an estimated 1.5% of users then, 7% by Docker's 2025 estimate. This is the second time the company has floated a harder line and retreated. A vendor that retreats twice is not a vendor that stopped caring about paid conversion; it is a vendor that learned the PaaS and CI workloads that actually hit limits are also the workloads it cannot afford to break loudly.
Why the Old Limits Still Bite — But Less Than the Horror Stories Claim
The classic scare math is real, but it assumes the fleet never authenticates.
Burst pulls = nodes × distinct images pulled per nodeA 50-node cluster rolling an app with 3 images triggers 150 manifest fetches in one burst. Anonymous, per-IP, that trips the 100/6hr limit mid-rollout and nodes start returning 429 Too Many Requests → ImagePullBackOff → ErrImagePull in kubectl get pods. That exact shape has been documented in MicroK8s, OSC shared-IP clusters, and every platform that lets kubelet pull as anonymous.
But that math quietly picks the worst of four variables. A representative fleet is not anonymous:
| Variable | Scare case | Representative case | Why it matters |
|---|---|---|---|
| Authentication | Anonymous (100/6hr per IP) | Authenticated free (200/6hr per account) or paid (unlimited / 5,000+/day on older Pro language) | Per-account limits isolate you from noisy neighbors sharing an egress IP; paid removes the ceiling entirely |
| Accounting scope | Per-IP (all nodes share one egress) | Per-account or per-registry-credential when you use imagePullSecrets | One NAT gateway serving 50 nodes makes 100/6hr a fleet-wide budget, not per-node. Authenticated pulls namespace the budget per credential |
| Layer deduplication | One pull per image per node | Registry and kubelet layer cache: base layers (e.g., alpine, distroless) fetched once per node, then hit local cache | A rollout that re-pulls myapp:v2 sharing 80% of layers with myapp:v1 fetches far fewer unique blobs than image count suggests |
| Steady state vs. burst | Full fleet rolls at once | Staggered rollouts, image cache warm, autoscaler adds 2-3 nodes at a time | Daily pull volume for a 10-node fleet running 30 images is ~30 pulls/day in steady state — well under 200/6hr — spikes only on fleet-wide updates |
Do the representative math:
- 10-node fleet, 30 images, weekly deploys, authenticated free: steady state ~30 pulls/day. A weekly rolling update that touches 5 images pulls ~50 fetches in a burst. Under 200/6hr, no budget pressure. Monitor
RateLimit-Remainingheaders; no mirror needed. - 30-node fleet, 3 images, all nodes share one egress IP, anonymous: 90 pulls in a single rollout. One retry or parallel DaemonSet pull trips 100/6hr before the rollout finishes. This is the only shape where a mirror pays for itself today — and it is fixed by one
imagePullSecretand a paid Docker subscription, not by a cache. - 30-node fleet, same rollout, authenticated paid: unlimited. The mirror's value drops from "prevent outage" to "save egress and speed pulls." Useful, but not load-bearing.
Kubernetes amplifies the cost of hitting the limit. A rescheduled pod does not know the image is already cached on the next node — kubelet asks the registry again. Cluster Autoscaler adding nodes, a Deployment with imagePullPolicy: Always, and a CI system that pushes :latest all turn one logical deploy into N physical pulls. That amplification is real, but it multiplies whatever budget you have; doubling a 100-pull budget to 200 by authenticating halves the amplification, and removing the ceiling with a paid subscription removes it.
The Six-Month Notice as a Real Mitigation
The most overlooked artifact in this whole episode is not a number. It is a sentence.
"We will announce any future enforcement at least 6 months in advance." — Docker, February 2025
And for storage:
"If charges are implemented in the future, we will give users at least six months' notice." — Docker via The Register
Three reasons to treat that as load-bearing, not marketing:
1. Docker has honored long lead times before. The November 2024 announcement gave a December-to-March window with no charges, plus plan changes only at each customer's next renewal after December 10. The February revision pushed enforcement another month to April 1. The company uses renewal-anchored rollouts precisely to avoid a flag-day break. Six months is consistent with that posture.
2. The cost of breaking CI/CD is asymmetric. Docker Hub is not a niche registry. It is the default FROM in millions of Dockerfiles and the default image source for MicroK8s, kind, and countless Helm charts. A surprise enforcement that breaks pulls breaks the README of the internet. Docker's estimate that only ~7% of users trip even the old limits means the other 93% are free to leave loudly if woken.
3. Six months is enough to build the mirror. A pull-through cache is a weekend project with a month of burn-in, not a quarter-long rewrite. That is the right unit to measure the guarantee against. If the mitigation takes six weeks and the notice is six months, the guarantee covers you with margin.
None of this means Docker will never charge. It means the risk of a zero-day pull-billing surprise is now bounded by a public commitment with a measurable build time on the other side. For a self-hosted PaaS deciding whether to spend operator time this quarter, a bounded risk with a known construction time is a different decision than an unbounded one.
If You Wait: Three Hardening Steps That Cost Almost Nothing
Waiting does not mean doing nothing. Three changes make "wait" an active strategy and turn the six-month notice into usable runway.
1. Authenticate every pull.
Stop letting kubelet pull as anonymous. Create one registry secret per namespace (or one global pull secret via imagePullSecrets in the ServiceAccount) and wire it into every workload. On a Cluster API fleet, bake the secret into the KubeadmControlPlane node bootstrap or the MachineDeployment so new nodes inherit it.
# create once
kubectl create secret docker-registry dockerhub \
--docker-server=https://index.docker.io/v1/ \
--docker-username=<user> --docker-password=<token> \
-n default
# attach to the default ServiceAccount so pods inherit it
kubectl patch serviceaccount default -n default \
-p '{"imagePullSecrets":[{"name":"dockerhub"}]}'One authenticated credential moves you from 100/6hr per-IP to 200/6hr per-account as free, or unlimited as paid Team/Business. That single change fixes the 50-node shared-IP burst.
2. Centralize and audit imagePullSecrets.
A secret in one namespace does not cover the next tenant's namespace. On a multi-tenant PaaS, the failure mode is "tenant B's Deployment has no imagePullSecrets and pulls anonymous." Audit it:
# which namespaces lack an image pull secret on the default SA?
kubectl get sa default -A -o json \
| jq -r '.items[] | select(.imagePullSecrets==null) | .metadata.namespace'
# which pods are still anonymous? look for 429s in events
kubectl get events -A --field-selector reason=Failed \
| grep -i "429\|pull.*limit\|Too Many Requests"If you run Harbor or GitHub Container Registry for tenant-built images, do the same there. The pattern is identical.
3. Monitor rate-limit headers before you hit them.
Docker Hub returns RateLimit-Limit and RateLimit-Remaining on manifest fetches. A one-line check in CI or a Prometheus exporter that scrapes the registry's header turns "unknown budget" into a dashboard. Sonatype, Kubermatic, and the Docker docs all recommend this over guessing.
TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" \
| jq -r .token)
curl -sI -H "Authorization: Bearer $TOKEN" \
https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest \
| grep -i ratelimitSet an alert at 50% of the 200/6hr window in any 6-hour rolling bucket. If you never fire it, the mirror stays on the shelf. If you fire it monthly, you have quantified evidence to justify the build — and six months to do it if Docker signals a change.
If You Mirror: The Minimal Viable Architecture and Its Real Cost
A pull-through cache is not hard to stand up. It is easy to under-operate. The minimal viable setup for a Cluster API fleet on Hetzner:
Option A — registry:2 as a pull-through proxy (simplest, Docker Hub only). Run the open-source distribution registry with proxy.remoteurl: https://registry-1.docker.io. Point kubelet's registry-mirrors or containerd's config.toml mirrors at the cache. First fetch proxies and caches; subsequent fetches serve from disk. Good for a single-tenant fleet that only needs Docker Hub.
Option B — Harbor or ECR pull-through (multi-registry). Harbor 2.x and AWS ECR both ship pull-through cache rules per upstream (Docker Hub, quay.io, ghcr.io, registry.k8s.io). If your fleet pulls from four registries, a single Harbor covers them with one credential vault and one GC policy. The Docker registry mirror mode only covers Docker Hub — for everything else kubelet still hits the upstream, which is the bandwidth-hog case the docker-registry-proxy projects specifically call out.
Option C — Spegel / Dragonfly / Kraken (P2P layer sharing, large fleets). For 50+ nodes, a peer-to-peer distribution layer (Spegel's distributed cache, Dragonfly's P2P) shares layers between nodes so the burst is N nodes × unique layers, not N × full images. Useful when the workload is not the rate limit but the egress and cold-start time.
What the mirror really costs:
| Cost | What it is | Typical value |
|---|---|---|
| Disk | Cache of unique layers, GC'd | 50-200 GB for a PaaS with 30-100 images, depends on base-image churn |
| HA | Cache is now a critical path — if it is down, pulls fail | Run 2 replicas + object-storage backend or accept that "no cache" falls back to upstream (configure fallback) |
| Credentials | One authenticated Docker Hub token stored once vs. per-namespace secrets | Central rotation, scope to read-only pull token |
| GC and freshness | Stale tags cached forever vs. TTL | Configure proxy.ttl or Harbor's tag-retention + nightly GC |
| Scope | Docker Hub only vs. multi-registry | Decide up front; a Docker-Hub-only mirror does not help ghcr.io pressure |
The honest accounting: a two-replica registry:2 on a Hetzner CPX31 with 100 GB volume and nightly GC costs a few euros a month and a half-day to wire into Cluster API's containerd config. Harbor with object storage costs more but replaces per-tenant secrets with one team. Neither is free to operate. Both are cheaper than debugging ImagePullBackOff across 30 tenants at 2 a.m. because the egress gateway's IP tripped a limit no one monitored.
The trigger to actually build it: you see 429 in events more than twice a quarter, you run >20 nodes behind one egress IP, or you need multi-registry caching for non-Docker-Hub pressure. Until then, keep it as a runbook.
The Bigger Pattern for a Self-Hosted PaaS
Docker's 2024-2025 arc is a small case of a larger dynamic self-hosters know well. Managed services add dimensions to the bill — pulls, storage, bandwidth, seats — until the invoice that used to be one number is a spreadsheet. Docker floated two new dimensions and shelved both. Fly.io added two new metered lines in early 2026. Vercel lifted overage by 33%. The direction is not "prices only ever fall."
A Cluster API fleet on owned Hetzner hardware inverts that shape. The monthly number is the hardware you already bought, the bandwidth that is included, and the disk that is yours. A registry mirror, when you need it, is a box you run — not a line you negotiate. That is not an argument for dogfooding a mirror before you need it. It is an argument for treating the six-month notice as infrastructure: a written SLA on business-model volatility with a construction time on the other side that you have measured.
Build the hardening now (auth, audit, alerts). Keep the mirror as a practiced runbook. Spend the operator time you saved on the storage or ingress layer that actually differentiates your platform.
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.
Sources
- Docker: Announcing Upgraded Docker Plans (November 2024) — original November plan update, December 10 / March 1 timelines, renewal-anchored rollouts.
- Docker: Revisiting Docker Hub Policies: Prioritizing Developer Experience (February 2025) — revised policy: unlimited pulls for paid subscribers, old limits retained for free/anonymous, at least six months' notice before future enforcement.
- The Register: Docker delays Hub pull limits, halts storage billing plan (February 2025) — interview with Docker VP on cancellation of consumption charges, indefinite delay of storage billing, six-month notice commitment.
- The New Stack: Revised Docker Hub Policies — Unlimited Pulls For All Paying Customers — summary of revised tiers and the 7% affected-user estimate.
- Docker Docs: Usage and Rate Limits and Docker Hub Pull Usage and Limits — current calculation basis (6-hour window), 429 behavior, pull accounting.
- Sonatype: Navigating Docker Hub Rate Limits with Nexus Repository — new April 1, 2025 tier language (10/hr anon, 100/hr free) and Nexus mitigation guidance.
- Red Hat: Mitigate Impact of Docker Hub Pull Request Limits — Kubernetes rescheduling and scaling amplification of pull limits.
- Komodor: How to Fix ErrImagePull and ImagePullBackOff and SpectralOps / Discuss Kubernetes threads —
ImagePullBackOffas rate-limit symptom, mitigation via authenticated pulls and mirrors.