Docker containerization provides consistent, portable, and scalable deployment environments for Django applications. This chapter covers Docker fundamentals, multi-stage builds, container orchestration, and production-ready Docker configurations for Django applications.
# Dockerfile
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=myproject.settings.production
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
postgresql-client \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
# Copy project
COPY . /app/
# Create non-root user
RUN adduser --disabled-password --gecos '' appuser
RUN chown -R appuser:appuser /app
USER appuser
# Collect static files
RUN python manage.py collectstatic --noinput
# Expose port
EXPOSE 8000
# Run the application
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]
# Dockerfile.production
# Build stage
FROM python:3.11-slim as builder
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Install build dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
git \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Production stage
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=myproject.settings.production
ENV PATH=/home/appuser/.local/bin:$PATH
WORKDIR /app
# Install runtime dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
postgresql-client \
nginx \
supervisor \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN adduser --disabled-password --gecos '' appuser
# Copy Python dependencies from builder stage
COPY --from=builder /root/.local /home/appuser/.local
# Copy application code
COPY --chown=appuser:appuser . /app/
# Create necessary directories
RUN mkdir -p /app/staticfiles /app/media /var/log/django \
&& chown -R appuser:appuser /app /var/log/django
# Switch to non-root user
USER appuser
# Collect static files
RUN python manage.py collectstatic --noinput
# Health check
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health/ || exit 1
EXPOSE 8000
# Use supervisor to manage multiple processes
USER root
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
volumes:
- .:/app
- static_volume:/app/staticfiles
- media_volume:/app/media
environment:
- DEBUG=1
- DJANGO_SETTINGS_MODULE=myproject.settings.development
- DATABASE_URL=postgresql://postgres:postgres@db:5432/django_app
- REDIS_URL=redis://redis:6379/1
depends_on:
- db
- redis
command: python manage.py runserver 0.0.0.0:8000
db:
image: postgres:13
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_DB=django_app
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
celery:
build: .
command: celery -A myproject worker -l info
volumes:
- .:/app
environment:
- DEBUG=1
- DJANGO_SETTINGS_MODULE=myproject.settings.development
- DATABASE_URL=postgresql://postgres:postgres@db:5432/django_app
- REDIS_URL=redis://redis:6379/1
depends_on:
- db
- redis
celery-beat:
build: .
command: celery -A myproject beat -l info
volumes:
- .:/app
environment:
- DEBUG=1
- DJANGO_SETTINGS_MODULE=myproject.settings.development
- DATABASE_URL=postgresql://postgres:postgres@db:5432/django_app
- REDIS_URL=redis://redis:6379/1
depends_on:
- db
- redis
volumes:
postgres_data:
static_volume:
media_volume:
# docker-compose.prod.yml
version: '3.8'
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- static_volume:/app/staticfiles:ro
- media_volume:/app/media:ro
- ./docker/nginx:/etc/nginx/conf.d:ro
- ./docker/ssl:/etc/ssl:ro
depends_on:
- web
restart: unless-stopped
web:
build:
context: .
dockerfile: Dockerfile.production
volumes:
- static_volume:/app/staticfiles
- media_volume:/app/media
environment:
- DJANGO_SETTINGS_MODULE=myproject.settings.production
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
- REDIS_URL=redis://redis:6379/1
- SECRET_KEY=${SECRET_KEY}
depends_on:
- db
- redis
restart: unless-stopped
deploy:
replicas: 3
resources:
limits:
memory: 512M
reservations:
memory: 256M
db:
image: postgres:13
volumes:
- postgres_data:/var/lib/postgresql/data/
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
restart: unless-stopped
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 512M
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
restart: unless-stopped
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 128M
celery:
build:
context: .
dockerfile: Dockerfile.production
command: celery -A myproject worker -l info --concurrency=4
volumes:
- media_volume:/app/media
environment:
- DJANGO_SETTINGS_MODULE=myproject.settings.production
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/1
- SECRET_KEY=${SECRET_KEY}
depends_on:
- db
- redis
restart: unless-stopped
deploy:
replicas: 2
resources:
limits:
memory: 512M
reservations:
memory: 256M
celery-beat:
build:
context: .
dockerfile: Dockerfile.production
command: celery -A myproject beat -l info
volumes:
- media_volume:/app/media
environment:
- DJANGO_SETTINGS_MODULE=myproject.settings.production
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/1
- SECRET_KEY=${SECRET_KEY}
depends_on:
- db
- redis
restart: unless-stopped
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 128M
monitoring:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./docker/prometheus:/etc/prometheus:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
restart: unless-stopped
volumes:
postgres_data:
redis_data:
static_volume:
media_volume:
prometheus_data:
networks:
default:
driver: bridge
# Dockerfile.optimized
FROM python:3.11-slim as base
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PIP_NO_CACHE_DIR=1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
# Install system dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
postgresql-client \
build-essential \
libpq-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Development stage
FROM base as development
WORKDIR /app
# Install development dependencies
COPY requirements/development.txt /app/requirements/
RUN pip install -r requirements/development.txt
# Copy source code
COPY . /app/
# Create non-root user
RUN adduser --disabled-password --gecos '' appuser \
&& chown -R appuser:appuser /app
USER appuser
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
# Production stage
FROM base as production
WORKDIR /app
# Install production dependencies only
COPY requirements/production.txt /app/requirements/
RUN pip install -r requirements/production.txt
# Copy source code
COPY . /app/
# Create non-root user and set permissions
RUN adduser --disabled-password --gecos '' appuser \
&& mkdir -p /app/staticfiles /app/media \
&& chown -R appuser:appuser /app
USER appuser
# Collect static files
RUN python manage.py collectstatic --noinput
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health/ || exit 1
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "myproject.wsgi:application"]
# Dockerfile.multiarch
FROM --platform=$BUILDPLATFORM python:3.11-slim as base
ARG TARGETPLATFORM
ARG BUILDPLATFORM
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Install dependencies based on architecture
RUN case "$TARGETPLATFORM" in \
"linux/amd64") \
apt-get update && apt-get install -y --no-install-recommends \
postgresql-client build-essential libpq-dev ;; \
"linux/arm64") \
apt-get update && apt-get install -y --no-install-recommends \
postgresql-client build-essential libpq-dev ;; \
*) echo "Unsupported platform: $TARGETPLATFORM" && exit 1 ;; \
esac \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy and install requirements
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY . .
# Create user and set permissions
RUN adduser --disabled-password --gecos '' appuser \
&& chown -R appuser:appuser /app
USER appuser
# Collect static files
RUN python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]
# docker/nginx/default.conf
upstream django {
server web:8000;
}
server {
listen 80;
server_name localhost;
# Security headers
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
# Static files
location /static/ {
alias /app/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
# Compression
gzip on;
gzip_types text/css application/javascript image/svg+xml;
}
# Media files
location /media/ {
alias /app/media/;
expires 30d;
add_header Cache-Control "public";
}
# Health check
location /health/ {
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;
access_log off;
}
# Main application
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;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
}
# docker/supervisord.conf
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:gunicorn]
command=/home/appuser/.local/bin/gunicorn --bind 0.0.0.0:8000 --workers 3 myproject.wsgi:application
directory=/app
user=appuser
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/django/gunicorn.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/nginx/nginx.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
# .env.prod
# Django
DJANGO_SETTINGS_MODULE=myproject.settings.production
SECRET_KEY=your-super-secret-key-here
DEBUG=False
# Database
POSTGRES_DB=django_app
POSTGRES_USER=postgres
POSTGRES_PASSWORD=secure_password_here
# Redis
REDIS_PASSWORD=redis_password_here
# Email
EMAIL_HOST=smtp.mailgun.org
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=postmaster@mg.yourdomain.com
EMAIL_HOST_PASSWORD=your-mailgun-password
# AWS (if using S3)
AWS_ACCESS_KEY_ID=your-aws-access-key
AWS_SECRET_ACCESS_KEY=your-aws-secret-key
AWS_STORAGE_BUCKET_NAME=your-s3-bucket
# Monitoring
SENTRY_DSN=https://...@sentry.io/...
# docker-compose.swarm.yml
version: '3.8'
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- static_volume:/app/staticfiles:ro
- media_volume:/app/media:ro
configs:
- source: nginx_config
target: /etc/nginx/conf.d/default.conf
deploy:
replicas: 2
placement:
constraints:
- node.role == manager
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
resources:
limits:
memory: 128M
reservations:
memory: 64M
web:
image: myapp:latest
environment:
- DJANGO_SETTINGS_MODULE=myproject.settings.production
secrets:
- db_password
- secret_key
volumes:
- static_volume:/app/staticfiles
- media_volume:/app/media
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
failure_action: rollback
order: start-first
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
resources:
limits:
memory: 512M
reservations:
memory: 256M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
db:
image: postgres:13
environment:
- POSTGRES_DB=django_app
- POSTGRES_USER=postgres
secrets:
- source: db_password
target: POSTGRES_PASSWORD
volumes:
- postgres_data:/var/lib/postgresql/data/
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
restart_policy:
condition: on-failure
resources:
limits:
memory: 1G
reservations:
memory: 512M
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
restart_policy:
condition: on-failure
resources:
limits:
memory: 256M
reservations:
memory: 128M
volumes:
postgres_data:
driver: local
redis_data:
driver: local
static_volume:
driver: local
media_volume:
driver: local
configs:
nginx_config:
file: ./docker/nginx/default.conf
secrets:
db_password:
external: true
secret_key:
external: true
networks:
default:
driver: overlay
attachable: true
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: django-app
---
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: django-config
namespace: django-app
data:
DJANGO_SETTINGS_MODULE: "myproject.settings.production"
DEBUG: "False"
DATABASE_URL: "postgresql://postgres:password@postgres:5432/django_app"
REDIS_URL: "redis://redis:6379/1"
---
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: django-secrets
namespace: django-app
type: Opaque
data:
SECRET_KEY: <base64-encoded-secret-key>
DB_PASSWORD: <base64-encoded-db-password>
---
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: django-app
namespace: django-app
spec:
replicas: 3
selector:
matchLabels:
app: django-app
template:
metadata:
labels:
app: django-app
spec:
containers:
- name: django
image: myapp:latest
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: django-config
- secretRef:
name: django-secrets
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health/
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready/
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
volumeMounts:
- name: static-volume
mountPath: /app/staticfiles
- name: media-volume
mountPath: /app/media
volumes:
- name: static-volume
persistentVolumeClaim:
claimName: static-pvc
- name: media-volume
persistentVolumeClaim:
claimName: media-pvc
---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: django-service
namespace: django-app
spec:
selector:
app: django-app
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: ClusterIP
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: django-ingress
namespace: django-app
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
tls:
- hosts:
- yourdomain.com
secretName: django-tls
rules:
- host: yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: django-service
port:
number: 80
---
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: django-hpa
namespace: django-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: django-app
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
# .github/workflows/docker-deploy.yml
name: Docker Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Build test image
run: |
docker build --target development -t django-test .
- name: Run tests
run: |
docker run --rm \
--network host \
-e DATABASE_URL=postgresql://postgres:postgres@localhost:5432/test_db \
-e REDIS_URL=redis://localhost:6379/1 \
django-test \
python manage.py test
- name: Run security checks
run: |
docker run --rm django-test bandit -r .
docker run --rm django-test safety check
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
target: production
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to production
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
script: |
cd /opt/django_app
docker-compose -f docker-compose.prod.yml pull
docker-compose -f docker-compose.prod.yml up -d
docker system prune -f
#!/bin/bash
# scripts/docker_build.sh - Docker build script
set -e
# Configuration
IMAGE_NAME="myapp"
REGISTRY="ghcr.io/username"
VERSION=${1:-latest}
echo "🐳 Building Docker image..."
# Build multi-stage image
docker build \
--target production \
--tag $IMAGE_NAME:$VERSION \
--tag $IMAGE_NAME:latest \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=$(git rev-parse --short HEAD) \
.
# Tag for registry
docker tag $IMAGE_NAME:$VERSION $REGISTRY/$IMAGE_NAME:$VERSION
docker tag $IMAGE_NAME:latest $REGISTRY/$IMAGE_NAME:latest
echo "✅ Docker image built successfully!"
# Push to registry if requested
if [ "$2" = "push" ]; then
echo "📤 Pushing to registry..."
docker push $REGISTRY/$IMAGE_NAME:$VERSION
docker push $REGISTRY/$IMAGE_NAME:latest
echo "✅ Image pushed to registry!"
fi
This comprehensive Docker guide provides everything needed to containerize Django applications effectively, from basic containers to production orchestration with Kubernetes and Docker Swarm.
Deploying on Linux Servers
Deploying Django applications on Linux servers provides maximum control, performance, and cost-effectiveness. This chapter covers comprehensive server setup, security hardening, service configuration, and deployment automation for Ubuntu, CentOS, and other Linux distributions.
Cloud Deployment Guides
Cloud deployment transforms Django applications from single-server setups to globally distributed, auto-scaling, and highly available systems. This comprehensive guide covers everything Django developers need to know about deploying to major cloud platforms, from basic concepts to advanced architectures, cost optimization, and production best practices.