DEV Community

Shivam Pawar
Shivam Pawar

Posted on

I Shaved 600 MB Off a Production Docker Image Here's What I Learned

I've been contributing to Eventyay an open-source event management platform by FOSSASIA and the first thing I did wasn't write a feature or fix a bug. I looked at the Dockerfile.

1.42 GB.

For a Django app. One point four two gigabytes. That's almost the size of a small Linux distro.

So naturally, I had to fix it.

What was going wrong

If you've worked with Docker and Django, you've probably made the same mistakes. I've definitely made them in my own projects before I knew better. The Eventyay Dockerfile had the classics:

Everything was in one stage. The image that gets shipped to production had build-essential, gcc, git, and a bunch of other tools that are only needed to compile packages not to run them. Once your Python packages are installed, you don't need a C compiler sitting in your production container.

The base image was heavy. The full python:3.12 image is around 900 MB by itself. That's before you add a single line of your own code.

apt cache was sticking around. Every apt-get install was leaving behind cached package lists. Small individually, but they add up.

The actual fix

The core idea is dead simple: build in one container, run in another.

Builder stage — do the heavy lifting

FROM python:3.12-slim-trixie AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /home/app/web

# These are ONLY needed to compile Python packages
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    git build-essential gettext make nodejs

# Install Python deps — this is where uv shines
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --locked --no-install-project --no-editable
Enter fullscreen mode Exit fullscreen mode

Quick note on uv if you haven't tried it — it's a Python package manager written in Rust, and it's fast. Like, "blink and it's done" fast. The --mount=type=cache line means it caches downloads between builds, so if you haven't changed your pyproject.toml, the entire dependency step is basically instant.

Final stage — only ship what runs

FROM python:3.12-slim-trixie

# No build-essential. No git. No gcc. Just runtime stuff.
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    netcat-traditional nodejs gettext less

COPY . /home/app/web

# Grab the venv from the builder — this is the magic line
COPY --from=builder /home/app/web/.venv /home/app/web/.venv

ENV PATH="/home/app/web/.venv/bin:$PATH"
Enter fullscreen mode Exit fullscreen mode

That COPY --from=builder line is doing all the work. It reaches into the builder container, grabs the .venv folder (which has all your installed packages), and drops it into a clean, slim image. The compiler, git, build headers all left behind.

What didn't make it to production

This is the stuff that was in the old image and doesn't need to be:

  • build-essential (~200 MB) — gcc, g++, make. Only needed to compile C extensions
  • git (~50 MB) — was used during build, not at runtime
  • Full Python image overhead (~400 MB) — switched to slim
  • pip/uv cache — cleaned up properly

The numbers

Before After
Image size 1.42 GB 820 MB
Cold build ~8 min ~5 min
Cached build ~8 min ~1 min

The cached build improvement is the one I love. If you're iterating on application code and your dependencies haven't changed, the expensive steps are completely skipped. Docker layer caching + uv's download cache = 1 minute builds.

Things I'd tell my past self

1. --no-install-recommends is not optional.
Without this flag, apt-get installs "recommended" packages that you almost certainly don't need. I've seen this flag alone save 80+ MB.

2. Slim images are fine. Really.
I used to worry that slim images would be missing something I needed. In practice, I've never had a runtime issue with them. If you need a specific library, just apt-get install it explicitly.

3. Think about layer order.
Dockerfile layers are cached top-to-bottom. Put stuff that changes rarely (system packages, Python deps) at the top. Put stuff that changes constantly (your code) at the bottom. I see a lot of Dockerfiles that COPY . . early and then install packages — meaning the package install runs on every single code change.

4. uv is worth trying.
I was a pip loyalist for years. uv won me over in one afternoon. The lockfile (uv.lock) gives you reproducible installs, and the speed is genuinely shocking the first time you see it. If you're curious, their Docker integration guide is solid.

If you want to do this to your own project

Honestly, start with these three things:

  1. Switch your base image from python:3.x to python:3.x-slim
  2. Add a builder stage and COPY --from=builder just your .venv
  3. Add --no-install-recommends to every apt-get install

That'll probably get you a 30-50% reduction right there.


This was my PR #2307 on FOSSASIA's Eventyay. If you're looking for open-source Django projects to contribute to, FOSSASIA is a good place to start — active maintainers, clear guidelines, and real production code.

I'm currently working toward GSoC 2026 with FOSSASIA and sharing the journey on X (@pawar_shiv59037). Feel free to follow along or say hi 👋

Top comments (0)