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
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"
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:
- Switch your base image from
python:3.xtopython:3.x-slim - Add a builder stage and
COPY --from=builderjust your.venv - Add
--no-install-recommendsto everyapt-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)