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..."
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
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"]
Key Dockerfile Best Practices
- Multi-stage builds — keeps final image lean (no build tools in production)
-
Non-root user — runs Django as
djangouser, not root (security!) -
Gunicorn — production WSGI server, never use
runserverin 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
# requirements/production.txt
-r base.txt
gunicorn==21.2.0
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'
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
# .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
# .gitignore — CRITICAL!
.env
*.pyc
__pycache__/
staticfiles/
mediafiles/
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:
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:
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;
}
}
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
Step 2 — Clone Your Project
git clone https://github.com/yourusername/myproject.git
cd myproject
Step 3 — Set Up Environment Variables
cp .env.example .env
nano .env # Fill in your production values
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
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
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
12. Common Pitfalls & Fixes
❌ Issue 1: Database not ready when Django starts
# Fix: Add healthcheck + depends_on condition
depends_on:
db:
condition: service_healthy
❌ Issue 2: Static files not showing
# Fix: Run collectstatic
docker compose exec web python manage.py collectstatic --noinput
❌ Issue 3: Migrations not applied
# Fix: Run migrations manually
docker compose exec web python manage.py migrate
❌ Issue 4: Secret key exposed in code
# Fix: Always use environment variables
from decouple import config
SECRET_KEY = config('SECRET_KEY') # Never hardcode!
❌ Issue 5: Container runs as root
# Fix: Always create non-root user
RUN adduser --system django
USER django
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)