Let’s be honest for a second. How many times have you written a Dockerfile for a Python app, run docker build, and stared in horror at a final image size north of 1 GB?
For a framework that champions clean, elegant code, our production Docker images often look like a digital junk drawer. We bundle build-time dependencies, system compilers (gcc), development headers, and cached pip wheels into the exact same container that’s supposed to run lean and mean in production.
If you are shipping a Django application to the cloud, bloating your container isn't just an aesthetic crime; it hurts your wallet and your security posture. Larger images take longer to pull on auto-scaling triggers, consume more storage on your container registry, and carry a larger attack surface with unnecessary system packages.
In here, we are going to fix that. We'll build a production-grade, secure, and lean Django Docker image using multi-stage builds and slim base images.
The Problem with the "Standard" Django Dockerfile
Take a look at a typical beginner Dockerfile for Django:
FROM python:3.11
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]
What’s wrong with this picture?
- The Heavy Base:
python:3.11uses a full Debian image containing hundreds of packages your Python web app will never need. - Build-Time Pollution: We install
build-essential(compilers and header files) just to compile dependencies likepsycopg2. But oncepip installfinishes, those compilers aren't needed anymore—yet they sit comfortably in our production layer, adding hundreds of megabytes. - Security Risk: Running containers as the
rootuser by default (which happens in many standard setups) combined with a bloated OS layer gives malicious actors a massive playground if a vulnerability is exploited.
The Solution: Multi-Stage Builds
Multi-stage builds allow us to use multiple FROM instructions in our Dockerfile. Each FROM uses a different base image, and we can copy artifacts (like compiled wheels or site-packages) from a previous stage into the final stage, leaving all the heavy build tools behind.
Here is our blueprint for a production-ready Django Docker setup.
Step 1: The Builder Stage
We start with a heavier image that contains everything required to compile our Python packages, install them into a target directory, and handle asset collection.
# --- Stage 1: Build dependencies ---
FROM python:3.11-slim AS builder
WORKDIR /app
# Install system build dependencies required for Python packages
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Install python dependencies into a local prefix directory
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
Notice the flag --prefix=/install. This tells pip to install everything cleanly into a specific folder so we can easily copy it over later.
Step 2: The Runtime Stage
Now, we spin up a fresh, pristine python:3.11-slim image. This stage knows nothing about build-essential or compilers. We simply copy the compiled Python packages from the builder stage, drop in our application code, and configure a secure non-root user.
# --- Stage 2: Final runtime image ---
FROM python:3.11-slim AS runtime
WORKDIR /app
# Install only the absolute runtime necessities (e.g., PostgreSQL client libraries)
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
# Copy installed python packages from the builder stage
COPY --from=builder /install /usr/local
# Create a non-privileged system user for security
RUN groupadd -g 1000 django && \
useradd -u 1000 -g django -s /bin/bash -m django
# Copy project code
COPY --chown=django:django . /app
# Switch to the non-root user
USER django
# Expose port and define entrypoint
EXPOSE 8000
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
Key Best Practices Packed Into This Setup
python:3.11-sliminstead ofpython:3.11: The slim variant strips out unnecessary Debian packages while still keeping a stable, standard Python runtime environment.--no-install-recommends: When usingapt-get, this prevents package managers from installing optional dependencies that bloat image layers.- Non-Root Execution (
USER django): Never run your production web server asroot. If an attacker finds a remote code execution vulnerability, they won't automatically gain root access to the underlying host container. --chown=django:djangoon Copy: Ensures that when project files (like static folders or media directories) are copied over, they belong to the correct non-root user.
Bonus: Speeding Up CI/CD Builds with BuildKit
If you are building your images in GitHub Actions, GitLab CI, or a cloud build pipeline, make sure you enable Docker BuildKit.
BuildKit intelligently parallelizes multi-stage builds, caches intermediate layers efficiently, and skips building stages that haven't changed.
To test your multi-stage build locally, run:
Bash
DOCKER_BUILDKIT=1 docker build -t my-django-app:latest .
Check your image size using docker images. You will likely notice your final image drop from 1.2 GB down to under 250 MB.
So,
Dockerizing Django doesn't have to mean shipping a bloated monolith of compilers and cache files. By adopting multi-stage builds, separating your build environment from your runtime environment, and enforcing strict user permissions, you get a faster, leaner, and significantly more secure production footprint.
Your container registry bill will thank you, your deployment pipelines will speed up, and you can sleep a little better knowing your infrastructure is buttoned up.
