Your Docker Image Is 1.2 GB — Here It Costs Downtime, Not Storage

·5 min read·Updated
dockerimagesbuilddeploymentperformance

Most advice about shrinking Docker images leads with storage cost. That reason doesn't apply here: your disk quota counts volume sizes, not image sizes. A 1.2 GB image doesn't push you toward the limit that stops your apps — only the data on your volumes does. But image size still costs you something more immediate: every redeploy stops your container, removes it, pulls the image, and starts a new one, with no overlap. A fat image makes that gap longer, makes every build slower, and makes the build itself more likely to fail against a memory ceiling. Slim images here buy you speed and reliability, not disk.

What actually counts against your disk limit

Worth being precise, because it's the opposite of what people assume: an application's disk usage is the sum of its volume sizes. Your image, your build layers, and the container's writable layer aren't part of that number.

That matters because the disk limit is the harshest one on the platform. Exceed it across your account and every app stops, and none will restart until total volume size drops back under. Nothing about a big image triggers that.

Unused images get pruned from nodes automatically as part of routine garbage collection, so an old image you've replaced isn't quietly accumulating against you either.

So if you're slimming an image to protect a quota, you're optimising the wrong thing. Here's what you're actually optimising.

Redeploys have downtime, and image size is part of it

There's no blue/green here. A redeploy runs in sequence:

  1. Your container gets a stop signal — and roughly one second later, a kill.
  2. The container is removed.
  3. The new image is pulled if it isn't already on the node.
  4. A fresh container is created and started.

Nothing overlaps. Your app is down from step 1 until your process is accepting work again in step 4, and the pull sits in the middle of that window. A 60 MB image and a 1.2 GB image produce very different gaps on a cold node.

For a bot, that gap is a few seconds of missed commands. For an HTTP service, it's requests that fail rather than wait. Neither is a catastrophe, but both are avoidable, and designing for restarts is easier when restarts are quick.

The build container is where fat images actually bite

Builds run in their own container with a smaller memory limit than your app — roughly 0.75 GB, and half a vCPU. A build that pulls a large base image, installs a compiler toolchain, and compiles native extensions is working against both.

Be clear about one thing, because it's a common misconception: multi-stage builds don't reduce the memory your build needs. They reduce the final image by leaving the toolchain behind in a discarded stage. The compile still happens, and it still has to fit. If your build dies partway through with no clear error, that's the memory limit, and the fix is smaller build steps or building in CI and pushing the image.

The four changes worth making

In descending order of payoff:

1. Multi-stage build. Compile in a stage with the full toolchain, copy only the artefact into a minimal runtime stage. This is the single biggest reduction available for compiled languages — a Go binary in a scratch or distroless image is a few megabytes, and Go is a good fit for exactly this reason. For a JVM app, the runtime stage should use a JRE, not a JDK.

2. A smaller base image, chosen carefully. python:3.12-slim instead of python:3.12 is usually free — the same glibc, far less baggage.

Alpine deserves a caveat, though, because "just use Alpine" is bad advice for Python. Alpine uses musl rather than glibc, and until relatively recently that meant no binary wheels at all. PEP 656 introduced musllinux wheel tags precisely because, as it puts it, distributions based on musl "cannot use the existing manylinux platform tags" — which left Alpine users compiling from source. musllinux wheels exist now but coverage is much thinner than manylinux, so on Alpine you're still more likely to fall back to a source build: slower, and hungrier for exactly the memory your build container doesn't have. A slim Debian base is usually the better trade for Python.

3. A .dockerignore file. Frequently the cheapest win, because build context gets copied before anything else runs:

.git
node_modules
__pycache__
*.pyc
.venv
tests/

.git alone can be larger than your application. This also protects you from COPY . . sweeping a local .env into the image — where secrets should never be.

4. Layer order that caches well. Copy your dependency manifest and install dependencies before copying your source:

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

Now a code change reuses the dependency layer instead of reinstalling everything. This doesn't shrink the image, but it's the change that most reduces how long you spend waiting on a git-push deploy. Note --no-cache-dir — pip's download cache is pure weight in an image.

Measure before you optimise

Two commands answer most questions:

docker images
docker history <image>

docker history shows the size of each layer, which usually points straight at the culprit — an apt-get install without rm -rf /var/lib/apt/lists/*, a cached wheel directory, a build artefact copied in and never removed. Fixing one 400 MB layer beats a dozen micro-optimisations.

Remember that deleting a file in a later layer doesn't shrink the image; the data is still in the earlier layer. Removals only help when they happen in the same RUN step that created the files.

FAQ

Does my Docker image count against my disk quota?

No. Disk usage is the sum of your volume sizes. Images and build layers aren't included.

Why should I shrink my image then?

Faster builds, and a shorter pull during redeploys — which is downtime, because containers are replaced with no overlap.

Do multi-stage builds fix build memory errors?

No. They shrink the final image by discarding the build stage, but the compile still runs and still has to fit within the build container's limit.

Should I use Alpine for Python?

Usually not. Thinner musl wheel coverage means more source builds, which are slower and use more memory. A slim Debian base is a better default.

What's the quickest way to find what's making my image big?

docker history <image>. It breaks the image down by layer so you can see which step added the weight.

Does a big image slow down my running app?

No. Once it's pulled and running, image size doesn't affect performance. It affects build and deploy time.

Wrapping up

The usual reason to slim a Docker image — paying for storage — isn't the reason here, because only your volumes count against the limit that matters. The real payoff is that a redeploy is a stop, a pull, and a start with nothing overlapping, so the image sits directly in your downtime window, and the build that produces it runs somewhere much smaller than your app. Multi-stage, a sensible base, a .dockerignore, and dependency layers before source code will get most images most of the way there in an afternoon.