Dockerizing Django REST Framework: From ‘Bulky’ Images to Lean Production Builds

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

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!

Share: