Your Docker Images Are Bleeding Your Bandwidth Budget Dry — Here's How to Stop It
Container infrastructure has become the default for serious development teams. Docker is table stakes. Kubernetes or a managed container platform is increasingly expected. CI/CD pipelines pull images dozens of times a day, distributed teams pull across regions, and nobody really thinks about what that costs — until the cloud bill lands and there's a line item that doesn't make sense.
That line item is egress. And for teams running containerized workloads, it can be substantial.
Let's walk through where the money is going and what you can actually do about it.
The Anatomy of an Expensive Image Pull
Start with a concrete example. Say your team has a Node.js application. Your Dockerfile starts with node:18, which is the official Node image. That base image is approximately 1.1 GB uncompressed. Add your application dependencies, build artifacts, and configuration, and you're probably looking at a final image somewhere between 1.5 and 2.5 GB.
Now think about how many times that image gets pulled:
- Every CI/CD pipeline run that doesn't have a warm cache pulls it fresh
- Every new developer environment pulls it during setup
- Every Kubernetes node that hasn't seen the image before pulls it at deployment
- Every staging environment refresh triggers a pull
For a reasonably active team running 50+ CI builds per day across multiple services, you can easily hit 100–200 GB of image pull traffic daily. At standard cloud egress rates — typically $0.08 to $0.09 per GB for outbound traffic from major providers — that's $8 to $18 per day, or $250 to $550 per month, just from container image traffic. For a startup watching every dollar, that's real money.
And that's before you account for cross-region pulls, which cost more and take longer.
Why Images Get So Big
Image bloat is almost always a consequence of convenience-first Dockerfile habits. The most common culprits:
Fat base images: The official python, node, and ruby images include compilers, package managers, documentation, and debugging tools that your production application doesn't need. The node:18 image is over 1 GB. The node:18-alpine variant is around 180 MB. That's an 80% reduction before you've changed a single line of your application code.
Build dependencies left in the final image: If your build process installs compilers or dev tools and you don't clean them up, they stay in the image layer permanently. Each RUN command that installs something and doesn't clean up afterward bakes that data into the layer.
Layer cache-busting through poor instruction ordering: Dockerfile instructions are cached per layer. If you copy your entire source directory before installing dependencies, every source code change invalidates the dependency layer cache — forcing a full reinstall on every build.
Unoptimized multi-stage builds (or no multi-stage at all): Multi-stage builds let you compile or build in one image and copy only the output artifacts into a minimal runtime image. Teams that don't use this pattern ship their entire build environment to production.
The Registry Architecture Problem
Beyond image size, the where of your registry matters enormously.
If your team is using Docker Hub as your primary registry and your infrastructure runs on AWS, you're paying egress every time a container pulls an image — because that traffic crosses the public internet and exits AWS's network. Docker Hub also introduced pull rate limits in 2020, which can throttle CI pipelines on free accounts.
The fix here is straightforward but underused: run a private registry in the same region as your compute infrastructure.
Options include:
- AWS ECR (Elastic Container Registry): No egress fees for pulls within the same AWS region. Integrates cleanly with ECS and EKS.
- Google Artifact Registry: Same deal for GCP workloads.
- GitHub Container Registry (GHCR): Reasonable option for teams already on GitHub Actions, though egress to non-GitHub infrastructure still applies.
- Self-hosted registries (Harbor, Nexus): More operational overhead, but gives you full control over caching, replication, and access policies.
Moving your primary registry into the same cloud region as your workloads is often the single highest-impact change you can make for both cost and pull latency.
Layer Caching in CI/CD: The Part Everyone Gets Wrong
CI/CD pipelines are the highest-frequency image consumers in most organizations, and they're also where caching is most frequently broken.
The default behavior for many CI systems — including GitHub Actions runners and fresh container environments — is to start with a clean state on every run. That means no Docker layer cache. Every build pulls the base image, reinstalls dependencies, and rebuilds from scratch.
Fixes that actually work:
Explicit cache mounts in GitHub Actions: The docker/build-push-action supports cache-from and cache-to parameters that can persist layer caches between runs using GitHub's cache storage or an external registry. Setting this up correctly can cut build times by 40–60% and dramatically reduce redundant pulls.
Registry-based layer caching: Push intermediate build stages to your registry with a stable tag (like build-cache:latest). Pull that tag at the start of subsequent builds and use it as the cache source. This works across different runner instances and doesn't depend on local disk state.
Pinning base image digests: Using node:18-alpine@sha256:... instead of node:18-alpine ensures you're pulling the exact same base layer every time, which dramatically improves cache hit rates.
Practical Reduction Targets
Here's a rough playbook with realistic impact estimates:
| Change | Typical Impact |
|---|---|
| Switch to Alpine or distroless base images | 60–80% image size reduction |
| Implement multi-stage builds | 40–70% image size reduction |
| Move registry to same cloud region as compute | 100% egress cost elimination for internal pulls |
| Implement CI layer caching | 40–60% reduction in CI pull volume |
| Combine all of the above | 70–85% total bandwidth cost reduction |
These aren't theoretical numbers. Teams that go through this exercise systematically consistently report hitting the 60–80% range in combined savings. For a team spending $400/month on image egress, that's $250–$320 back in the budget every month.
The Audit You Should Run This Week
If you're not sure where to start, here's a practical first pass:
- Pull your registry metrics: Most registries (ECR, GHCR, Docker Hub) provide pull count and data transfer stats. Find out how much data your team is actually moving.
- Audit your Dockerfile for the big wins: Check your base image size, look for build tools in the final stage, and verify you're using multi-stage builds for compiled languages.
- Check your CI cache configuration: Look at a recent build log and confirm whether layers are being cached or rebuilt from scratch.
- Map where your registry lives relative to your compute: If they're in different regions or different clouds, you're paying for it.
Container infrastructure is supposed to make deployment faster and more reliable. The bandwidth costs that come with it don't have to be a surprise — they're a solvable problem, and most of the solutions are a few Dockerfile lines and a configuration change away.