Dockerizing Django Applications: Don’t Just Stop at ‘It Works’
A common sight: You finish building a Docker image for Django and are horrified to see it’s over 1GB. Deployment to the server is slow, and RAM alerts keep flashing red because of dozens of redundant build tools.
I was once in a similar situation while managing a system of over 30 containers for an e-commerce platform. At that point, optimization wasn’t just a hobby—it was a survival requirement to save on cloud costs. By applying multi-stage builds, I managed to shrink the image from 900MB down to 180MB. CI/CD time also dropped from 5 minutes to less than 45 seconds.
This article will share a standard framework for bringing your Django REST Framework (DRF) projects to production in the most professional way possible.
Quick Start: Up and Running in 5 Minutes
If you’re in a hurry, create a Dockerfile and docker-compose.yml in your root directory following the structure below to see immediate results.
.
├── core/ (Django project)
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── .env
Spin up the entire stack with a single command:
docker-compose up --build
Once you see the app running smoothly, let’s dissect how to optimize every bit to turn it into a high-performance machine.
1. The Multi-stage Build Trick: Slimming Down Your Image
Why are Python images usually heavy? It’s because libraries like psycopg2 or Pillow require gcc and musl-dev for compilation. These tools are bulky and completely useless once the app is running.
# Stage 1: Builder - Where libraries are 'cooked'
FROM python:3.11-slim as builder
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update && apt-get install -y \
build-essential libpq-dev --no-install-recommends
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# Stage 2: Final - Keep only what's necessary to run
FROM python:3.11-slim
WORKDIR /app
# Create a non-root user for security
RUN addgroup --system app && adduser --system --group app
RUN apt-get update && apt-get install -y libpq-dev --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/wheels /wheels
RUN pip install --no-cache /wheels/*
COPY . .
RUN chown -R app:app /app
USER app
CMD ["gunicorn", "core.wsgi:application", "--bind", "0.0.0.0:8000"]
This way, the entire build toolset (gcc, headers) is left behind in Stage 1. Your final image will be extremely lean and more secure because it doesn’t run with root privileges.
2. Docker Compose: Bridging Web, Celery, and Redis
A real-world DRF system always needs background workers to handle heavy tasks. The key is using the same Dockerfile for both the Web app and the Celery worker. This ensures a 100% consistent environment across services.
version: '3.8'
services:
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file: .env
redis:
image: redis:7-alpine
web:
build: .
command: gunicorn core.wsgi:application --bind 0.0.0.0:8000
volumes: [ ".:/app" ]
ports: [ "8000:8000" ]
env_file: .env
depends_on: [ db, redis ]
worker:
build: .
command: celery -A core worker --loglevel=info
env_file: .env
depends_on: [ db, redis ]
volumes:
postgres_data:
A small note: depends_on only ensures the DB container starts first; it doesn’t wait for the DB to be ready to accept connections. In production environments, you should use a wait-for-it script.
3. Handling Entrypoint and Automated Migrations
Never run migrations manually on the server. Let Docker handle it every time it starts via an entrypoint.sh file.
#!/bin/sh
# Check if DB is ready before running migrations
python manage.py migrate --noinput
python manage.py collectstatic --noinput
exec "$@"
Don’t forget the chmod +x entrypoint.sh command. Without this step, your container will throw a ‘Permission denied’ error upon startup.
4. Real-world Tips to Avoid Headaches
Eliminate Junk Files with .dockerignore
Every file you copy into the image increases size and security risks. Create a .dockerignore to exclude .git, __pycache__, and unnecessary environment files like .env.
Resource Limits
A memory leak bug in Django can bring down an entire VPS. Always limit the RAM for each service in your compose file. For example, memory: 512M is a reasonable figure for a mid-sized Django instance.
Smart Log Management
By default, Docker stores logs as JSON, and they will swell until the hard drive is full. Configure max-size: "10m" so Docker automatically rotates logs, preventing the server from crashing in the middle of the night.
Summary
Properly Dockerizing Django isn’t just about writing a Dockerfile that works. It’s the art of balancing image size, build speed, and security.
With the multi-stage framework and Docker Compose organization shown above, you have a solid foundation to scale your app to dozens of containers without the chaos. For larger projects, consider adding Nginx to handle static files and SSL. Happy deploying!

