Complete Docker Guide for Django & DRF
What is Docker and Why Use It?

Docker in a Nutshell:
Docker is a containerization platform that packages applications and their dependencies into standardized units called containers.
Traditional vs Dockerized Deployment:
TRADITIONAL: "It works on my machine!"
Developer Machine → "Works!"
Production Server → "But not here! 😭"
DOCKERIZED: "It works everywhere!"
Developer Machine (Container) → "Works!"
CI/CD Pipeline (Container) → "Works!"
Production Server (Container) → "Works! 🎉"
Key Benefits for Django Development:
| Benefit | Description |
| Consistency | Same environment everywhere |
| Isolation | No "dependency hell" |
| Reproducibility | Recreate exact environment |
| Scalability | Easy to scale horizontally |
| Version Control | Track environment changes |
| Microservices | Break app into independent services |
🏗️ Docker Core Concepts
1. Docker Images vs Containers:
Image: Blueprint/recipe (like a class in OOP)
Container: Running instance (like an object instance)
2. Dockerfile: Instructions to build an image
3. Docker Compose: Orchestrate multi-container apps
4. Volumes: Persistent data storage
5. Networks: Communication between containers
🚀 Quick Start: Simple Django + Docker
Project Structure:
django_docker_project/
├── .dockerignore
├── .env
├── .env.example
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── manage.py
├── entrypoint.sh
├── core/
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── apps/
│ └── api/
└── static/
Step 1: Basic Dockerfile
# Dockerfile
# ------------- STAGE 1: Builder -------------
FROM python:3.11-slim as builder
# Set work directory
WORKDIR /app
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
libpq-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# ------------- STAGE 2: Final -------------
FROM python:3.11-slim
# Create non-root user
RUN addgroup --system django && \
adduser --system --ingroup django django
# Create directory for app
WORKDIR /home/django/app
# Install dependencies
COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/requirements.txt .
RUN pip install --no-cache /wheels/*
# Copy project
COPY --chown=django:django . .
# Switch to non-root user
USER django
# Run entrypoint
COPY --chown=django:django entrypoint.sh .
RUN chmod +x entrypoint.sh
ENTRYPOINT ["/home/django/app/entrypoint.sh"]
Step 2: Entrypoint Script
#!/bin/bash
# entrypoint.sh
set -o errexit
set -o pipefail
set -o nounset
# Function to check if PostgreSQL is ready
postgres_ready() {
python << END
import sys
import psycopg2
from decouple import config
try:
psycopg2.connect(
dbname=config("DB_NAME"),
user=config("DB_USER"),
password=config("DB_PASSWORD"),
host=config("DB_HOST"),
port=config("DB_PORT", default=5432),
)
except psycopg2.OperationalError:
sys.exit(-1)
sys.exit(0)
END
}
# Function to check if Redis is ready
redis_ready() {
python << END
import sys
import redis
from decouple import config
try:
redis_client = redis.Redis(
host=config("REDIS_HOST", default="redis"),
port=config("REDIS_PORT", default=6379),
db=config("REDIS_DB", default=0),
socket_connect_timeout=5
)
redis_client.ping()
except redis.exceptions.ConnectionError:
sys.exit(-1)
sys.exit(0)
END
}
# Wait for PostgreSQL
until postgres_ready; do
echo "Waiting for PostgreSQL to become available..."
sleep 2
done
echo "PostgreSQL is available!"
# Wait for Redis (if needed)
until redis_ready; do
echo "Waiting for Redis to become available..."
sleep 2
done
echo "Redis is available!"
# Run migrations
python manage.py migrate --noinput
# Collect static files
python manage.py collectstatic --noinput
# Create superuser if doesn't exist (for development)
if [ "$DJANGO_SUPERUSER_EMAIL" ] && [ "$DJANGO_SUPERUSER_USERNAME" ]; then
python manage.py createsuperuser \
--noinput \
--email "$DJANGO_SUPERUSER_EMAIL" \
--username "$DJANGO_SUPERUSER_USERNAME" || true
fi
# Execute command passed to docker run
exec "$@"
Step 3: Docker Compose for Development
# docker-compose.yml
version: '3.8'
services:
# Django Application
web:
build: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/home/django/app
- static_volume:/home/django/app/staticfiles
- media_volume:/home/django/app/media
ports:
- "8000:8000"
env_file:
- .env
environment:
- DJANGO_SETTINGS_MODULE=core.settings.dev
depends_on:
- db
- redis
restart: unless-stopped
networks:
- django_network
# PostgreSQL Database
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
env_file:
- .env
environment:
- POSTGRES_DB=${DB_NAME}
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
ports:
- "5432:5432"
networks:
- django_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
# Redis for caching and Celery
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
ports:
- "6379:6379"
networks:
- django_network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# Celery Worker
celery_worker:
build: .
command: celery -A core worker --loglevel=info
volumes:
- .:/home/django/app
env_file:
- .env
environment:
- DJANGO_SETTINGS_MODULE=core.settings.dev
depends_on:
- db
- redis
- web
networks:
- django_network
restart: unless-stopped
# Celery Beat (for scheduled tasks)
celery_beat:
build: .
command: celery -A core beat --loglevel=info
volumes:
- .:/home/django/app
env_file:
- .env
environment:
- DJANGO_SETTINGS_MODULE=core.settings.dev
depends_on:
- db
- redis
- web
networks:
- django_network
restart: unless-stopped
# Nginx for production-like setup
nginx:
image: nginx:1.25-alpine
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- static_volume:/home/django/app/staticfiles:ro
- media_volume:/home/django/app/media:ro
ports:
- "80:80"
depends_on:
- web
networks:
- django_network
volumes:
postgres_data:
redis_data:
static_volume:
media_volume:
networks:
django_network:
driver: bridge
Step 4: Environment Variables
# .env.example
# Copy to .env and fill in values
# Django
SECRET_KEY=your-secret-key-here
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
DJANGO_SETTINGS_MODULE=core.settings.dev
# Database
DB_NAME=django_db
DB_USER=django_user
DB_PASSWORD=django_password
DB_HOST=db
DB_PORT=5432
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DB=0
# Celery
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0
# Superuser (for development)
DJANGO_SUPERUSER_EMAIL=admin@example.com
DJANGO_SUPERUSER_USERNAME=admin
DJANGO_SUPERUSER_PASSWORD=admin123
# Email (for production)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
# CORS
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
Step 5: Docker Ignore File
# .dockerignore
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
.venv/
# Django
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
media/
# Coverage
.coverage
htmlcov/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Docker
Dockerfile
docker-compose*.yml
.env
⚡ Advanced Django Docker Configuration
Multi-stage Dockerfile for Production
# Dockerfile.prod
# ------------- STAGE 1: Builder -------------
FROM python:3.11-slim as builder
WORKDIR /app
# Install system dependencies for building
RUN apt-get update && apt-get install -y \
gcc \
g++ \
libpq-dev \
libjpeg-dev \
libpng-dev \
libwebp-dev \
zlib1g-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Install Python dependencies
COPY requirements/prod.txt .
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r prod.txt
# ------------- STAGE 2: Runtime -------------
FROM python:3.11-slim
# Install runtime dependencies only
RUN apt-get update && apt-get install -y \
libpq-dev \
libjpeg-dev \
libpng-dev \
libwebp-dev \
nginx \
gettext \
&& rm -rf /var/lib/apt/lists/*
# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Create non-root user
RUN addgroup --system --gid 1001 django && \
adduser --system --uid 1001 --gid 1001 django
# Create app directory
WORKDIR /home/django/app
# Copy application
COPY --chown=django:django . .
# Collect static files
RUN python manage.py collectstatic --noinput
# Configure nginx
COPY --chown=django:django nginx/nginx.conf /etc/nginx/nginx.conf
COPY --chown=django:django nginx/django.conf /etc/nginx/sites-available/django
RUN ln -s /etc/nginx/sites-available/django /etc/nginx/sites-enabled/ && \
rm /etc/nginx/sites-enabled/default
# Switch to non-root user
USER django
# Expose port
EXPOSE 8000
# Run Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--threads", "2", "core.wsgi:application"]
Production Docker Compose
# docker-compose.prod.yml
version: '3.8'
services:
# Django with Gunicorn
web:
build:
context: .
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:8000 --workers 4 --threads 2 --access-logfile - core.wsgi:application
expose:
- 8000
volumes:
- static_volume:/home/django/app/staticfiles
- media_volume:/home/django/app/media
env_file:
- .env.prod
environment:
- DJANGO_SETTINGS_MODULE=core.settings.production
depends_on:
- db
- redis
restart: always
networks:
- backend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health/"]
interval: 30s
timeout: 10s
retries: 3
# Nginx
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- static_volume:/home/django/app/staticfiles:ro
- media_volume:/home/django/app/media:ro
- ./logs/nginx:/var/log/nginx
depends_on:
- web
networks:
- backend
- frontend
restart: always
# PostgreSQL
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
- ./postgres/backups:/backups
env_file:
- .env.prod
environment:
- POSTGRES_DB=${DB_NAME}
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
command: >
postgres -c max_connections=200
-c shared_buffers=256MB
-c effective_cache_size=1GB
networks:
- backend
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
# Redis
redis:
image: redis:7-alpine
command: >
redis-server
--appendonly yes
--maxmemory 256mb
--maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf:ro
networks:
- backend
restart: always
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# Celery Worker
celery_worker:
build:
context: .
dockerfile: Dockerfile.prod
command: celery -A core worker --loglevel=info --concurrency=4
volumes:
- media_volume:/home/django/app/media
env_file:
- .env.prod
environment:
- DJANGO_SETTINGS_MODULE=core.settings.production
depends_on:
- db
- redis
- web
networks:
- backend
restart: always
deploy:
replicas: 2 # Scale to 2 workers
# Celery Beat
celery_beat:
build:
context: .
dockerfile: Dockerfile.prod
command: celery -A core beat --loglevel=info
env_file:
- .env.prod
environment:
- DJANGO_SETTINGS_MODULE=core.settings.production
depends_on:
- db
- redis
- web
networks:
- backend
restart: always
# Flower for monitoring Celery
flower:
build:
context: .
dockerfile: Dockerfile.prod
command: celery -A core flower --port=5555
ports:
- "5555:5555"
env_file:
- .env.prod
environment:
- DJANGO_SETTINGS_MODULE=core.settings.production
depends_on:
- celery_worker
- redis
networks:
- backend
restart: always
# pgAdmin (Database GUI - optional)
pgadmin:
image: dpage/pgadmin4
environment:
- PGADMIN_DEFAULT_EMAIL=admin@example.com
- PGADMIN_DEFAULT_PASSWORD=admin123
- PGADMIN_CONFIG_SERVER_MODE=False
ports:
- "5050:80"
volumes:
- pgadmin_data:/var/lib/pgadmin
- ./pgadmin/servers.json:/pgadmin4/servers.json:ro
networks:
- backend
restart: always
# Traefik as reverse proxy (alternative to nginx)
traefik:
image: traefik:v3.0
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=admin@example.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
- "8080:8080" # Traefik dashboard
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
networks:
- frontend
restart: always
volumes:
postgres_data:
redis_data:
static_volume:
media_volume:
pgadmin_data:
networks:
frontend:
driver: bridge
backend:
driver: bridge
🔧 Django Settings for Docker
Multi-environment Settings Structure:
# core/settings/__init__.py
import os
# Import the appropriate settings based on environment
environment = os.environ.get('DJANGO_SETTINGS_MODULE', 'core.settings.dev')
if environment == 'core.settings.production':
from .production import *
else:
from .dev import *
Development Settings:
# core/settings/dev.py
import os
from pathlib import Path
from decouple import config
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Security
SECRET_KEY = config('SECRET_KEY', default='dev-secret-key')
DEBUG = True
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1').split(',')
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME', default='django_db'),
'USER': config('DB_USER', default='django_user'),
'PASSWORD': config('DB_PASSWORD', default='django_password'),
'HOST': config('DB_HOST', default='db'),
'PORT': config('DB_PORT', default=5432),
}
}
# Cache
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': f"redis://{config('REDIS_HOST', default='redis')}:{config('REDIS_PORT', default=6379)}/1",
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
},
'KEY_PREFIX': 'django_dev',
}
}
# Static files
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# CORS
CORS_ALLOWED_ORIGINS = config(
'CORS_ALLOWED_ORIGINS',
default='http://localhost:3000,http://127.0.0.1:3000'
).split(',')
# Email (Console for development)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Celery
CELERY_BROKER_URL = config('CELERY_BROKER_URL', default='redis://redis:6379/0')
CELERY_RESULT_BACKEND = config('CELERY_RESULT_BACKEND', default='redis://redis:6379/0')
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC'
# DRF
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}
# Debug Toolbar (only in dev)
if DEBUG:
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE.insert(0, 'debug_toolbar.middleware.DebugToolbarMiddleware')
INTERNAL_IPS = ['127.0.0.1', 'localhost']
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': lambda request: True,
}
Production Settings:
# core/settings/production.py
import os
from pathlib import Path
from decouple import config
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Security
SECRET_KEY = config('SECRET_KEY')
DEBUG = False
ALLOWED_HOSTS = config('ALLOWED_HOSTS').split(',')
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASSWORD'),
'HOST': config('DB_HOST'),
'PORT': config('DB_PORT', default=5432),
'CONN_MAX_AGE': 600,
'OPTIONS': {
'sslmode': 'require' if config('DB_SSL', default=False, cast=bool) else 'disable',
},
}
}
# Cache
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': config('REDIS_URL'),
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 50,
'retry_on_timeout': True,
},
'COMPRESSOR': 'django_redis.compressors.zlib.ZlibCompressor',
},
'KEY_PREFIX': 'django_prod',
}
}
# Static files (served by nginx)
STATIC_URL = '/static/'
STATIC_ROOT = '/home/django/app/staticfiles'
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = '/home/django/app/media'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' # For S3
# Security headers
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# CORS
CORS_ALLOWED_ORIGINS = config('CORS_ALLOWED_ORIGINS').split(',')
CORS_ALLOW_CREDENTIALS = True
# Email
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = config('EMAIL_HOST')
EMAIL_PORT = config('EMAIL_PORT', cast=int)
EMAIL_HOST_USER = config('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool)
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default=EMAIL_HOST_USER)
# Celery
CELERY_BROKER_URL = config('CELERY_BROKER_URL')
CELERY_RESULT_BACKEND = config('CELERY_RESULT_BACKEND')
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC'
CELERY_TASK_ACKS_LATE = True
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
CELERY_TASK_REJECT_ON_WORKER_LOST = True
# DRF
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day',
},
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 50,
}
# Sentry
if config('SENTRY_DSN', default=''):
sentry_sdk.init(
dsn=config('SENTRY_DSN'),
integrations=[DjangoIntegration()],
traces_sample_rate=0.1,
send_default_pii=True,
environment=config('SENTRY_ENVIRONMENT', default='production'),
)
# Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
'file': {
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/var/log/django/django.log',
'maxBytes': 1024 * 1024 * 5, # 5 MB
'backupCount': 5,
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': True,
},
'django.request': {
'handlers': ['console', 'file'],
'level': 'ERROR',
'propagate': False,
},
'myapp': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': True,
},
},
}
🌐 Nginx Configuration
Nginx Config for Django:
# nginx/nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# Basic settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=5r/m;
# Include site configurations
include /etc/nginx/sites-enabled/*;
}
Django Site Configuration:
# nginx/sites-available/django
# HTTP Server
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
# HTTPS Server
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL certificates
ssl_certificate /etc/nginx/ssl/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/yourdomain.com/privkey.pem;
# SSL optimization
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Static files
location /static/ {
alias /home/django/app/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Media files
location /media/ {
alias /home/django/app/media/;
expires 30d;
add_header Cache-Control "public";
access_log off;
}
# API endpoints with rate limiting
location /api/ {
# Rate limiting for API
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://web:8000;
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 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# Authentication endpoints with stricter limits
location ~ ^/api/(auth|login|register|password-reset)/ {
limit_req zone=auth_limit burst=5 nodelay;
proxy_pass http://web:8000;
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;
}
# Admin panel
location /admin/ {
proxy_pass http://web:8000;
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;
# Basic authentication for admin (optional extra layer)
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/.htpasswd;
}
# Main Django app
location / {
proxy_pass http://web:8000;
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;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Health check endpoint
location /health/ {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
🔐 Security Configuration
Production Entrypoint with Security:
#!/bin/bash
# entrypoint.prod.sh
set -e
# Run as non-root user
if [ "$(id -u)" = '0' ]; then
# Change ownership of app directory
chown -R django:django /home/django/app
# Drop privileges
exec gosu django "$0" "$@"
fi
# Wait for database
echo "Waiting for PostgreSQL..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
# Wait for Redis
echo "Waiting for Redis..."
while ! nc -z $REDIS_HOST $REDIS_PORT; do
sleep 0.1
done
echo "Redis started"
# Run database migrations
echo "Running migrations..."
python manage.py migrate --noinput
# Collect static files
echo "Collecting static files..."
python manage.py collectstatic --noinput
# Create cache table
python manage.py createcachetable
# Create superuser if not exists
if [ -n "$DJANGO_SUPERUSER_EMAIL" ] && [ -n "$DJANGO_SUPERUSER_PASSWORD" ]; then
echo "Creating superuser..."
python manage.py shell << END
import os
from django.contrib.auth import get_user_model
User = get_user_model()
if not User.objects.filter(email=os.environ['DJANGO_SUPERUSER_EMAIL']).exists():
User.objects.create_superuser(
email=os.environ['DJANGO_SUPERUSER_EMAIL'],
username=os.environ.get('DJANGO_SUPERUSER_USERNAME', 'admin'),
password=os.environ['DJANGO_SUPERUSER_PASSWORD']
)
print("Superuser created successfully!")
else:
print("Superuser already exists.")
END
fi
# Start Gunicorn
echo "Starting Gunicorn..."
exec gunicorn "$@"
📊 Monitoring and Logging
Docker Logging Configuration:
# docker-compose.monitoring.yml
version: '3.8'
services:
# Prometheus for metrics
prometheus:
image: prom/prometheus:v2.45.0
container_name: prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
- '--web.console.templates=/usr/share/prometheus/consoles'
- '--storage.tsdb.retention.time=200h'
- '--web.enable-lifecycle'
expose:
- 9090
ports:
- "9090:9090"
networks:
- monitoring
restart: unless-stopped
# Grafana for dashboards
grafana:
image: grafana/grafana:10.0.0
container_name: grafana
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
expose:
- 3000
ports:
- "3000:3000"
networks:
- monitoring
restart: unless-stopped
depends_on:
- prometheus
# Node Exporter for system metrics
node-exporter:
image: prom/node-exporter:v1.6.0
container_name: node-exporter
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.rootfs=/rootfs'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
expose:
- 9100
networks:
- monitoring
restart: unless-stopped
# cAdvisor for container metrics
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.47.0
container_name: cadvisor
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
expose:
- 8080
ports:
- "8080:8080"
networks:
- monitoring
restart: unless-stopped
# Loki for logs
loki:
image: grafana/loki:2.8.2
container_name: loki
volumes:
- ./loki/loki-config.yaml:/etc/loki/local-config.yaml:ro
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
expose:
- 3100
networks:
- monitoring
restart: unless-stopped
# Promtail for log collection
promtail:
image: grafana/promtail:2.8.2
container_name: promtail
volumes:
- ./promtail/promtail-config.yaml:/etc/promtail/config.yaml:ro
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yaml
networks:
- monitoring
restart: unless-stopped
depends_on:
- loki
volumes:
prometheus_data:
grafana_data:
loki_data:
networks:
monitoring:
driver: bridge
Prometheus Configuration:
# prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets: []
rule_files: []
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'django'
static_configs:
- targets: ['web:8000']
metrics_path: '/metrics/'
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
- job_name: 'postgres'
static_configs:
- targets: ['postgres-exporter:9187']
- job_name: 'redis'
static_configs:
- targets: ['redis-exporter:9121']
⚡ Performance Optimization
1. Docker Build Optimization:
# Use .dockerignore effectively
# Use multi-stage builds
# Order commands properly (least changing first)
# Use specific base image tags (not 'latest')
2. Django Gunicorn Configuration:
# gunicorn.conf.py
import multiprocessing
# Server socket
bind = "0.0.0.0:8000"
backlog = 2048
# Worker processes
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
# Logging
accesslog = "-"
errorlog = "-"
loglevel = "info"
# Process naming
proc_name = "django_gunicorn"
# Server mechanics
daemon = False
pidfile = None
umask = 0
user = None
group = None
tmp_upload_dir = None
# SSL (if needed)
# keyfile = "/path/to/key.pem"
# certfile = "/path/to/cert.pem"
# Worker processes
preload_app = True
# Max requests per worker
max_requests = 1000
max_requests_jitter = 50
# Graceful timeout
graceful_timeout = 30
# Worker class for async (if using async views)
# worker_class = "uvicorn.workers.UvicornWorker"
3. Database Connection Pooling:
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASSWORD'),
'HOST': config('DB_HOST'),
'PORT': config('DB_PORT', default=5432),
'CONN_MAX_AGE': 600, # 10 minutes
'OPTIONS': {
'connect_timeout': 10,
'keepalives': 1,
'keepalives_idle': 30,
'keepalives_interval': 10,
'keepalives_count': 5,
},
'POOL_OPTIONS': {
'POOL_SIZE': 20,
'MAX_OVERFLOW': 10,
'RECYCLE': 3600,
},
}
}
🔧 Development Workflow Commands
Common Docker Commands:
# Build and start containers
docker-compose up -d --build
# View logs
docker-compose logs -f web
docker-compose logs -f db
docker-compose logs -f nginx
# Execute commands in container
docker-compose exec web python manage.py migrate
docker-compose exec web python manage.py createsuperuser
docker-compose exec db psql -U django_user -d django_db
# Django management commands
docker-compose exec web python manage.py shell
docker-compose exec web python manage.py test
docker-compose exec web python manage.py makemigrations
# Database operations
docker-compose exec db pg_dump -U django_user django_db > backup.sql
docker-compose exec -T db psql -U django_user django_db < backup.sql
# Container management
docker-compose down # Stop containers
docker-compose down -v # Stop and remove volumes
docker-compose restart web # Restart specific service
docker-compose ps # List containers
docker-compose top # Show running processes
# Clean up
docker system prune -a # Remove unused containers, images, networks
docker volume prune # Remove unused volumes
# Production commands
docker-compose -f docker-compose.prod.yml up -d --build
docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic
Makefile for Automation:
# Makefile
.PHONY: help build up down logs shell migrate test clean
help:
@echo "Available commands:"
@echo " make build - Build Docker images"
@echo " make up - Start containers"
@echo " make down - Stop containers"
@echo " make logs - View logs"
@echo " make shell - Open Django shell"
@echo " make migrate - Run migrations"
@echo " make test - Run tests"
@echo " make clean - Clean Docker resources"
build:
docker-compose build
up:
docker-compose up -d
down:
docker-compose down
logs:
docker-compose logs -f
shell:
docker-compose exec web python manage.py shell
migrate:
docker-compose exec web python manage.py migrate
makemigrations:
docker-compose exec web python manage.py makemigrations
test:
docker-compose exec web python manage.py test
test-coverage:
docker-compose exec web python -m pytest --cov=. --cov-report=html
clean:
docker-compose down -v
docker system prune -af
# Production
prod-up:
docker-compose -f docker-compose.prod.yml up -d --build
prod-down:
docker-compose -f docker-compose.prod.yml down
prod-logs:
docker-compose -f docker-compose.prod.yml logs -f
# Database backup
backup:
docker-compose exec db pg_dump -U django_user django_db > backup_$(shell date +%Y%m%d_%H%M%S).sql
# Restore database
restore:
@read -p "Enter backup file name: " file; \
docker-compose exec -T db psql -U django_user django_db < $$file
🚀 Deployment Strategies
1. Single Server Deployment:
# docker-compose.deploy.yml
version: '3.8'
services:
web:
image: your-registry/django-app:${TAG:-latest}
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
order: start-first
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
networks:
- traefik-public
environment:
- DATABASE_URL=postgres://...
- REDIS_URL=redis://...
labels:
- "traefik.enable=true"
- "traefik.http.routers.django.rule=Host(`yourdomain.com`)"
- "traefik.http.routers.django.entrypoints=websecure"
- "traefik.http.routers.django.tls.certresolver=myresolver"
2. Zero-Downtime Deployment Script:
#!/bin/bash
# deploy.sh
set -e
# Load environment
source .env.prod
# Build and push image
docker build -t your-registry/django-app:${COMMIT_SHA} -f Dockerfile.prod .
docker push your-registry/django-app:${COMMIT_SHA}
# Update stack
docker stack deploy -c docker-compose.deploy.yml django-stack
# Wait for health checks
sleep 30
# Check service health
docker service ps django-stack_web --format "{{.CurrentState}}"
# Remove old images
docker image prune -f
3. Docker Swarm Deployment:
# Initialize swarm
docker swarm init
# Deploy stack
docker stack deploy -c docker-compose.swarm.yml django
# Check services
docker service ls
# Scale services
docker service scale django_web=5
# Update service
docker service update --image your-registry/django-app:new-version django_web
🔍 Debugging Dockerized Django
Common Issues and Solutions:
1. Database Connection Issues:
# Check if database is accessible
docker-compose exec web python -c "
import psycopg2
try:
conn = psycopg2.connect(
dbname='django_db',
user='django_user',
password='django_password',
host='db',
port=5432
)
print('Database connection successful')
except Exception as e:
print(f'Database connection failed: {e}')
"
2. Check Container Logs:
# See detailed logs
docker-compose logs --tail=100 web
docker-compose logs --tail=100 db
# Follow logs in real-time
docker-compose logs -f web
# Check specific service
docker-compose exec web python manage.py check --deploy
3. Debug Memory Issues:
# Check container resource usage
docker stats
# Inspect container
docker inspect django_web_1
# Check processes inside container
docker-compose exec web top
# Memory profiling
docker-compose exec web python -m memory_profiler your_script.py
4. Network Issues:
# Check network connectivity
docker-compose exec web ping db
docker-compose exec web curl http://web:8000/health/
# List networks
docker network ls
docker network inspect django_docker_project_default
# Test port exposure
nc -zv localhost 8000
📦 Docker Hub and Registry
Push to Docker Hub:
# Login to Docker Hub
docker login
# Build and tag image
docker build -t yourusername/django-app:latest -t yourusername/django-app:1.0.0 .
# Push to Docker Hub
docker push yourusername/django-app:latest
docker push yourusername/django-app:1.0.0
# Pull in production
docker pull yourusername/django-app:latest
Private Registry Setup:
# Run private registry
docker run -d -p 5000:5000 --name registry registry:2
# Tag and push to private registry
docker tag django-app localhost:5000/django-app:latest
docker push localhost:5000/django-app:latest
# Pull from private registry
docker pull localhost:5000/django-app:latest
🔐 Security Best Practices
1. Docker Security Scanning:
# Scan for vulnerabilities
docker scan your-image:tag
# Use Snyk
docker run -v /var/run/docker.sock:/var/run/docker.sock snyk/snyk:docker --file Dockerfile
# Use Trivy
docker run aquasec/trivy image your-image:tag
2. Security Hardening:
# Use non-root user
RUN adduser --disabled-password --gecos '' django
USER django
# Copy with correct permissions
COPY --chown=django:django . /app
# Use .dockerignore to exclude sensitive files
# Remove unnecessary packages
RUN apt-get purge -y gcc g++ && apt-get autoremove -y
# Use specific versions
FROM python:3.11-slim@sha256:abc123...
# Scan image regularly
# Keep base images updated
# Use multi-stage builds to reduce attack surface
3. Secret Management:
# docker-compose.yml with secrets
services:
web:
image: django-app:latest
secrets:
- django_secret_key
- db_password
environment:
- SECRET_KEY_FILE=/run/secrets/django_secret_key
- DB_PASSWORD_FILE=/run/secrets/db_password
secrets:
django_secret_key:
file: ./secrets/django_secret_key.txt
db_password:
file: ./secrets/db_password.txt
🎯 CI/CD Pipeline Example
GitHub Actions Workflow:
# .github/workflows/docker-ci-cd.yml
name: Django Docker CI/CD
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run migrations
env:
DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres
REDIS_URL: redis://redis:6379/0
run: |
python manage.py migrate
- name: Run tests
env:
DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres
REDIS_URL: redis://redis:6379/0
SECRET_KEY: test-secret-key
run: |
python manage.py test
- name: Security scan
run: |
pip install bandit safety
bandit -r .
safety check -r requirements.txt
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.prod
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/django-app:latest
${{ secrets.DOCKER_USERNAME }}/django-app:${{ github.sha }}
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/django-app:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/django-app:buildcache,mode=max
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- 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 pull ${{ secrets.DOCKER_USERNAME }}/django-app:latest
docker-compose -f docker-compose.prod.yml down
docker-compose -f docker-compose.prod.yml up -d
docker system prune -af
📚 Cheat Sheet
Docker Commands:
# Image Management
docker build -t myapp .
docker images
docker rmi myapp
docker image prune
# Container Management
docker run -d -p 8000:8000 myapp
docker ps
docker ps -a
docker stop container_id
docker rm container_id
docker exec -it container_id bash
# Docker Compose
docker-compose up -d
docker-compose down
docker-compose logs -f
docker-compose exec service_name command
docker-compose ps
docker-compose restart
# Cleanup
docker system prune -a
docker volume prune
docker network prune
# Monitoring
docker stats
docker top container_id
docker logs container_id
Django Docker Commands:
# Development
docker-compose exec web python manage.py migrate
docker-compose exec web python manage.py createsuperuser
docker-compose exec web python manage.py shell
docker-compose exec web python manage.py test
# Production
docker-compose -f docker-compose.prod.yml up -d --build
docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic
docker-compose -f docker-compose.prod.yml logs -f web
# Database
docker-compose exec db psql -U user -d dbname
docker-compose exec db pg_dump -U user dbname > backup.sql
✅ Production Checklist
Before Deployment:
Security Scanning: Scan Docker images for vulnerabilities
Environment Variables: All sensitive data in env files
Database Backups: Backup strategy in place
Monitoring: Set up logging and monitoring
SSL/TLS: Configure HTTPS
Backup Strategy: Database and media files
Load Testing: Test under production-like load
Disaster Recovery: Plan for failures
After Deployment:
Health Checks: Verify all services are healthy
Performance: Monitor response times
Security: Regular security updates
Backups: Verify backup integrity
Scaling: Monitor and scale as needed
Updates: Regular Docker and Django updates
🚨 Common Pitfalls and Solutions
| Pitfall | Solution |
| Database not ready | Use health checks and wait-for scripts |
| Permission issues | Use non-root user and correct file permissions |
| Volume mounting | Use named volumes for production data |
| Memory leaks | Set memory limits and monitor usage |
| Build cache issues | Use multi-stage builds and proper .dockerignore |
| Slow builds | Use build cache and optimize Dockerfile order |
| Network issues | Use Docker networks and correct service names |
| Timeouts | Increase timeouts in Django and Docker |
📈 Performance Metrics to Monitor
# Django metrics endpoint
# urls.py
from django_prometheus.exports import ExportToDjangoView
urlpatterns = [
path('metrics/', ExportToDjangoView, name='prometheus-django-metrics'),
]
# Monitor:
# - Request latency (p50, p95, p99)
# - Database query performance
# - Cache hit rates
# - Memory usage per worker
# - CPU utilization
# - Queue lengths (Celery)
# - Error rates
# - Response status codes
🎉 Congratulations!
You now have a complete Docker setup for Django/DRF that covers:
✅ Development environment with hot-reload
✅ Production setup with optimization
✅ Multi-container orchestration
✅ Security best practices
✅ Monitoring and logging
✅ CI/CD pipeline
✅ Scaling strategies
Remember: Start simple, iterate, and always test your Docker configurations thoroughly. The power of Docker is that you can evolve your setup as your application grows.
Happy Dockering! 🐳🚀
![Docker + Django in 10 Days: From Zero to Production [Day: 07]](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F696fcd472484e6d591510582%2F911a802d-c837-4cb6-b3fc-73966aff1020.png&w=3840&q=75)
![Docker + Django in 10 Days: From Zero to Production [Day: 06]](/_next/image?url=https%3A%2F%2Fcloudmate-test.s3.us-east-1.amazonaws.com%2Fuploads%2Fcovers%2F696fcd472484e6d591510582%2Fa53f784c-27e6-4611-8627-a7d76fe9875b.png&w=3840&q=75)
![Docker + Django in 10 Days: From Zero to Production [Day: 05]](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1770578634367%2F8708279a-574a-4250-b12b-22d01fe91841.png&w=3840&q=75)
![Docker + Django in 10 Days: From Zero to Production [Day: 04]](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1770151845620%2F77951891-5216-4440-aab8-7b848b6eba8f.png&w=3840&q=75)
![Docker + Django in 10 Days: From Zero to Production [Day: 03]](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1769895181687%2Fdd60bdd1-f050-4f82-8e47-dde4a70f7798.png&w=3840&q=75)