H
Himashi Meththasena
Guest
Let’s be honest for a second. How many times have you written a
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 (
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.
Take a look at a typical beginner Dockerfile for Django:
What’s wrong with this picture?
Multi-stage builds allow us to use multiple
Here is our blueprint for a production-ready Django Docker setup.
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.
Notice the flag
Now, we spin up a fresh, pristine
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
Check your image size using
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.
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:
Code:
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.
Code:
# --- 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.
Code:
# --- 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
Code:
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.