Your Docker Registry Is Bleeding Money — And Nobody On Your Team Noticed
Somewhere in your organization right now, there's a container registry quietly racking up charges like a hotel minibar that nobody locked. Nobody authorized it. Nobody's watching it. And at the end of the month, it shows up on an AWS or GCP bill line that everyone assumes someone else is responsible for.
This is the container registry tax — and if your team has been shipping Docker-based workloads for more than a year, you're almost certainly paying it.
How It Starts (And Why Nobody Catches It Early)
The problem rarely starts with a bad decision. It starts with a good one: you tag your images properly, you push to a registry, and CI/CD does its thing. That's solid engineering practice. The issue is what happens after the pipeline runs.
Most teams set up their image tagging strategy once and never revisit it. You end up with patterns like my-service:build-1482 sitting next to my-service:build-1483 all the way up to my-service:build-3917. Every single one of those images is stored in full, taking up gigabytes of space that you're paying for by the GB-month.
AWS ECR charges around $0.10 per GB per month. Google Artifact Registry is in a similar ballpark. That sounds modest until you realize a single Node.js production image can easily run 800MB to 1.2GB, and you might be storing hundreds of historical builds that no running container has referenced in eight months.
Do the math. It adds up faster than most people expect.
The Three Big Culprits
1. Orphaned Images From Dead Projects
Every organization that's been building software for a few years has graveyards — projects that were deprecated, pivoted away from, or just quietly abandoned. The code might be archived in GitHub. The Jira board might be closed. But the Docker images? Those are still sitting in the registry, accumulating storage costs like rent on an empty apartment.
Because nobody explicitly owns cleanup duty, nobody does it. The images aren't hurting anything (or so it seems), so they stay. Multiply this across a dozen internal services and a handful of experimental projects, and you've got a significant chunk of dead weight.
2. Forgotten Namespaces and Repositories
This one is especially common in larger engineering orgs where different teams spin up their own registry namespaces. A platform team creates platform/base-images. A data engineering squad creates data/etl-workers. A contractor engagement creates vendor/third-party-tools. The contractor finishes the engagement. The data team migrates to a different pipeline architecture. The platform team consolidates their base images.
The namespaces stick around. The repositories inside them stick around. Nobody has a full map of what's in the registry anymore, and nobody's going to go looking until someone gets a surprise on next quarter's cloud spend report.
3. Versioning Strategies That Made Sense Once
This is the sneakiest one. A lot of teams adopted a "never delete, always append" philosophy around images because immutability feels safe. And in some contexts, it is — you want to be able to roll back to a known good state quickly. But "keep everything forever" is a very different policy from "keep the last 30 builds."
Without an explicit retention policy, you're effectively keeping everything forever by default. Registries don't expire images automatically. That's your job, and most teams haven't done it.
Running the Audit: A Practical Framework
Okay, so how do you actually get a handle on this? Here's a straightforward audit process you can run in an afternoon.
Step 1: Pull a full inventory. Use your registry's CLI or API to list every repository and image. For ECR, aws ecr describe-repositories followed by aws ecr list-images per repo will get you there. For GCP, gcloud artifacts repositories list is your starting point. Export this to a spreadsheet or pipe it into a simple script.
Step 2: Identify last-pull dates. Most registries track when an image was last pulled. This is your most important data point. Any image that hasn't been pulled in 90+ days is a strong candidate for deletion. Anything past 180 days with no active deployment reference is almost certainly safe to remove.
Step 3: Cross-reference with running workloads. Before you delete anything, validate against your running containers. In Kubernetes environments, you can query kubectl get pods -A -o jsonpath='{range .items[*]}{.spec.containers[*].image}{"\n"}{end}' to get a list of every image currently in use. Build a "protected" list from this output.
Step 4: Map repositories to active teams. For each repository, figure out who owns it. If you can't find an owner after a reasonable effort, that's a signal the project may be dead. Tag these as candidates for archival review.
Step 5: Estimate your savings before you touch anything. Add up the total size of images you've flagged for deletion. Multiply by your per-GB-month rate. This number will make the case for building a proper retention policy going forward — and might get some eyebrows raised in the right direction.
Building a Retention Policy That Actually Sticks
The audit is a one-time fix. The retention policy is how you stop this from happening again.
A reasonable baseline policy for most teams looks something like this: keep the last 20 builds for any active service, keep any image tagged as latest or with a semantic version tag (like v2.4.1), and automatically expire untagged images after 14 days. Most registries support lifecycle policies natively — ECR's lifecycle policy JSON, for example, makes this pretty straightforward to configure.
For repositories tied to inactive or deprecated services, set a shorter window — maybe 30 days from the last pull — and then let the lifecycle policy clean up after itself.
Document the policy in your engineering handbook. Make it part of your service offboarding checklist. When a team decommissions a service, cleaning up the registry should be as automatic as archiving the repo.
The Culture Shift Underneath the Technical Fix
Here's the uncomfortable truth: the container registry tax isn't really a technical problem. It's a culture and ownership problem. Nobody set out to waste money on orphaned images. It happened because cleanup work is invisible, it doesn't ship features, and it doesn't show up in sprint velocity metrics.
The teams that solve this long-term are the ones that treat infrastructure hygiene as a first-class engineering concern — not a quarterly fire drill. That means tagging registry resources with team ownership, including storage cost awareness in architecture reviews, and giving engineers visibility into the cloud spend their code generates.
Your registry bill is a mirror. What it usually reflects is a team that's been moving fast and not looking back. That's not a character flaw — it's a natural consequence of shipping pressure. But once you see the number, it's hard to unsee it.
Run the audit. Build the policy. And next time someone asks why the cloud bill went down, you'll have a genuinely satisfying answer.