Forem

Cover image for Django + Docker Deployment: A Complete Production Guide
sribalu
sribalu

Posted on

Django + Docker Deployment: A Complete Production Guide

Django + Docker Deployment: A Complete Production Guide

Level: Intermediate | Read Time: 14 min | Author: Sri Balu


Introduction

Deploying Django applications used to mean wrestling with server configurations, dependency conflicts, and "it works on my machine" nightmares. Docker changes all of that.

In this guide, we'll containerize a Django application from scratch, wire it up with PostgreSQL and Nginx, and deploy it to a production server — the right way.

Here's what we'll cover:

  • Why Docker for Django applications
  • Writing a production-ready Dockerfile
  • Docker Compose for multi-container setup (Django + PostgreSQL + Nginx)
  • Environment variables and secrets management
  • Static files and media files handling
  • Running database migrations automatically
  • Deploying to a Linux server (Ubuntu)
  • Common pitfalls and how to avoid them

1. Why Docker for Django?

Before we write any code, let's understand why Docker has become the standard for Django deployments.

The Problem Without Docker

Developer A: "It works on my machine!"
Developer B: "But it crashes on mine..."
DevOps:      "It works in staging but not production..."
Enter fullscreen mode Exit fullscreen mode

What Docker Solves

Problem Docker Solution
Dependency conflicts Isolated containers per service
Environment differences Same image runs everywhere
Complex setup One command: docker compose up
Scaling Spin up multiple containers easily
Rollback Switch back to previous image instantly

2. Project Structure

Here's the structure we'll build:

myproject/
├── app/
│   ├── manage.py
│   ├── myproject/
│   │   ├── settings/
│   │   │   ├── base.py
│   │   │   ├── development.py
│   │   │   └── production.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   └── requirements/
│       ├── base.txt
│       └── production.txt
├── nginx/
│   └── nginx.conf
├── .env
├── .env.example
├── docker-compose.yml
├── docker-compose.prod.yml
└── Dockerfile
Enter fullscreen mode Exit fullscreen mode

3. Writing a Production-Ready Dockerfile

A good Dockerfile uses multi-stage builds to keep the final image small and secure.

# Dockerfile

# ─── Stage 1: Builder ───────────────────────────────────────
FROM python:3.12-slim AS builder

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# Install Python dependencies
COPY requirements/base.txt requirements/production.txt ./requirements/
RUN pip install --upgrade pip \
    && pip install -r requirements/production.txt

# ─── Stage 2: Production Image ──────────────────────────────
FROM python:3.12-slim AS production

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

# Install only runtime dependencies
RUN apt-get update && apt-get install -y \
    libpq5 \
    && rm -rf /var/lib/apt/lists/*

# Copy installed packages from builder
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

# Copy project files
COPY app/ .

# Create non-root user for security
RUN addgroup --system django \
    && adduser --system --ingroup django django \
    && chown -R django:django /app

USER django

# Expose port
EXPOSE 8000

# Start Gunicorn
CMD ["gunicorn", "myproject.wsgi:application", \
     "--bind", "0.0.0.0:8000", \
     "--workers", "3", \
     "--timeout", "120"]
Enter fullscreen mode Exit fullscreen mode

Key Dockerfile Best Practices

  • Multi-stage builds — keeps final image lean (no build tools in production)
  • Non-root user — runs Django as django user, not root (security!)
  • Gunicorn — production WSGI server, never use runserver in production
  • PYTHONUNBUFFERED=1 — ensures logs appear in real time

4. Requirements Files

# requirements/base.txt
Django==5.0.4
djangorestframework==3.15.1
psycopg2==2.9.9
python-decouple==3.8
whitenoise==6.6.0
Enter fullscreen mode Exit fullscreen mode
# requirements/production.txt
-r base.txt
gunicorn==21.2.0
Enter fullscreen mode Exit fullscreen mode

5. Django Settings for Docker

Split your settings into base and production:

# app/myproject/settings/base.py
from decouple import config

SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='').split(',')

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': config('POSTGRES_DB'),
        'USER': config('POSTGRES_USER'),
        'PASSWORD': config('POSTGRES_PASSWORD'),
        'HOST': config('POSTGRES_HOST', default='db'),
        'PORT': config('POSTGRES_PORT', default='5432'),
    }
}

# Static files
STATIC_URL = '/static/'
STATIC_ROOT = '/app/staticfiles'

MEDIA_URL = '/media/'
MEDIA_ROOT = '/app/mediafiles'

# Whitenoise for static files
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',  # Add this!
    ...
]

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
Enter fullscreen mode Exit fullscreen mode

6. Environment Variables (.env)

Never hardcode secrets! Use a .env file:

# .env (never commit this to git!)
SECRET_KEY=your-super-secret-key-here
DEBUG=False
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com

# PostgreSQL
POSTGRES_DB=myproject_db
POSTGRES_USER=myproject_user
POSTGRES_PASSWORD=strong_password_here
POSTGRES_HOST=db
POSTGRES_PORT=5432
Enter fullscreen mode Exit fullscreen mode
# .env.example (commit this — no real secrets)
SECRET_KEY=your-secret-key
DEBUG=False
ALLOWED_HOSTS=localhost
POSTGRES_DB=myproject_db
POSTGRES_USER=myproject_user
POSTGRES_PASSWORD=your-password
POSTGRES_HOST=db
POSTGRES_PORT=5432
Enter fullscreen mode Exit fullscreen mode
# .gitignore — CRITICAL!
.env
*.pyc
__pycache__/
staticfiles/
mediafiles/
Enter fullscreen mode Exit fullscreen mode

7. Docker Compose — Development

# docker-compose.yml (development)
version: '3.9'

services:
  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - .env
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

  web:
    build:
      context: .
      target: builder  # Use builder stage for dev (has dev tools)
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ./app:/app  # Live code reloading
    ports:
      - "8000:8000"
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy

volumes:
  postgres_data:
Enter fullscreen mode Exit fullscreen mode

8. Docker Compose — Production

# docker-compose.prod.yml (production)
version: '3.9'

services:
  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - .env
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

  web:
    build:
      context: .
      target: production  # Use production stage
    command: >
      sh -c "python manage.py migrate --noinput &&
             python manage.py collectstatic --noinput &&
             gunicorn myproject.wsgi:application
             --bind 0.0.0.0:8000
             --workers 3
             --timeout 120"
    volumes:
      - static_volume:/app/staticfiles
      - media_volume:/app/mediafiles
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  nginx:
    image: nginx:1.25-alpine
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
      - static_volume:/app/staticfiles
      - media_volume:/app/mediafiles
    ports:
      - "80:80"
    depends_on:
      - web
    restart: unless-stopped

volumes:
  postgres_data:
  static_volume:
  media_volume:
Enter fullscreen mode Exit fullscreen mode

9. Nginx Configuration

Nginx acts as a reverse proxy — it handles static files and forwards API requests to Gunicorn:

# nginx/nginx.conf
upstream django {
    server web:8000;
}

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    client_max_body_size 20M;

    # Static files — served directly by Nginx (fast!)
    location /static/ {
        alias /app/staticfiles/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # Media files
    location /media/ {
        alias /app/mediafiles/;
    }

    # Django app — proxy to Gunicorn
    location / {
        proxy_pass http://django;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 60s;
        proxy_read_timeout 60s;
    }
}
Enter fullscreen mode Exit fullscreen mode

10. Deploying to Ubuntu Server

Step 1 — Install Docker on Server

# Update system
sudo apt-get update
sudo apt-get upgrade -y

# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Install Docker Compose
sudo apt-get install docker-compose-plugin -y

# Add your user to docker group
sudo usermod -aG docker $USER
newgrp docker

# Verify installation
docker --version
docker compose version
Enter fullscreen mode Exit fullscreen mode

Step 2 — Clone Your Project

git clone https://github.com/yourusername/myproject.git
cd myproject
Enter fullscreen mode Exit fullscreen mode

Step 3 — Set Up Environment Variables

cp .env.example .env
nano .env  # Fill in your production values
Enter fullscreen mode Exit fullscreen mode

Step 4 — Build and Start

# Build images
docker compose -f docker-compose.prod.yml build

# Start all services
docker compose -f docker-compose.prod.yml up -d

# Check status
docker compose -f docker-compose.prod.yml ps
Enter fullscreen mode Exit fullscreen mode

Step 5 — Verify Everything Works

# Check logs
docker compose -f docker-compose.prod.yml logs web
docker compose -f docker-compose.prod.yml logs nginx

# Run a command inside the container
docker compose -f docker-compose.prod.yml exec web python manage.py createsuperuser
Enter fullscreen mode Exit fullscreen mode

11. Useful Docker Commands

# View running containers
docker compose ps

# View logs (follow mode)
docker compose logs -f web

# Restart a service
docker compose restart web

# Stop all services
docker compose down

# Stop and remove volumes (WARNING: deletes DB data!)
docker compose down -v

# Rebuild after code changes
docker compose up --build -d

# Run Django management commands
docker compose exec web python manage.py migrate
docker compose exec web python manage.py shell
docker compose exec web python manage.py createsuperuser

# Access PostgreSQL directly
docker compose exec db psql -U myproject_user -d myproject_db
Enter fullscreen mode Exit fullscreen mode

12. Common Pitfalls & Fixes

❌ Issue 1: Database not ready when Django starts

# Fix: Add healthcheck + depends_on condition
depends_on:
  db:
    condition: service_healthy
Enter fullscreen mode Exit fullscreen mode

❌ Issue 2: Static files not showing

# Fix: Run collectstatic
docker compose exec web python manage.py collectstatic --noinput
Enter fullscreen mode Exit fullscreen mode

❌ Issue 3: Migrations not applied

# Fix: Run migrations manually
docker compose exec web python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

❌ Issue 4: Secret key exposed in code

# Fix: Always use environment variables
from decouple import config
SECRET_KEY = config('SECRET_KEY')  # Never hardcode!
Enter fullscreen mode Exit fullscreen mode

❌ Issue 5: Container runs as root

# Fix: Always create non-root user
RUN adduser --system django
USER django
Enter fullscreen mode Exit fullscreen mode

Summary

Here's what we built:

Component Technology
Web server Gunicorn + Django
Reverse proxy Nginx
Database PostgreSQL 16
Containerization Docker + Docker Compose
Static files Whitenoise + Nginx
Secrets python-decouple + .env
Security Non-root user, no DEBUG in prod

What's Next?

  • Part 3: Django REST API with PostgreSQL — Advanced Queries & Optimization
  • Part 4: CI/CD Pipeline for Django with GitHub Actions
  • Part 5: Scaling Django with Redis, Celery & Background Tasks

Found this helpful? Share it with your team! Questions? Drop them in the comments.


Tags: django docker python devops postgresql nginx deployment backend

Top comments (0)