Django remains the go-to framework for Python developers building robust web applications. Its “batteries-included” philosophy provides everything needed for production-ready applications, but deploying Django requires careful consideration of database connections, static files, security settings, and scaling strategies. This guide covers everything from basic Django deployments to advanced production configurations.
Note: This guide focuses on deploying Django on Ubuntu servers. CloudPloy currently supports Laravel applications, with Django support coming soon. Stay tuned for updates!
Understanding Django Hosting Requirements
Django applications have specific deployment needs that differ from simple static sites:
- WSGI/ASGI Server: Production-grade application server
- Database Management: PostgreSQL, MySQL, or other backends
- Static File Serving: Efficient CDN distribution
- Media File Handling: User-uploaded content management
- Background Tasks: Celery workers for async processing
- Caching Layer: Redis or Memcached for performance
- Security Configuration: Production security settings
CloudPloy provides a Django-optimized infrastructure with automatic configuration for all these components.
Deploy Django in 60 Seconds
Quick Start Deployment
# Clone your Django project
git clone https://github.com/yourusername/django-app.git
cd django-app
# Install CloudPloy CLI
pip install cloudploy-cli
# Initialize deployment
cloudploy init --framework django
# Deploy to production
cloudploy deploy
# Your Django app is live!
# URL: https://django-app.ploy.app
Production-Ready Django Settings
# settings/production.py
import os
from .base import *
import dj_database_url
# Security
DEBUG = False
ALLOWED_HOSTS = [
'.ploy.app',
'yourdomain.com',
'www.yourdomain.com'
]
SECRET_KEY = os.environ.get('SECRET_KEY')
# Database
DATABASES = {
'default': dj_database_url.config(
default=os.environ.get('DATABASE_URL'),
conn_max_age=600,
conn_health_checks=True,
)
}
# Security Settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Static Files
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Media Files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Database configuration
DATABASES = {
'default': dj_database_url.parse(config('DATABASE_URL'))
}
# Redis cache configuration
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': config('REDIS_URL'),
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
# Session configuration
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_AGE = 86400 # 1 day
# CSRF configuration
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
# AWS S3 Configuration (CloudPloy managed)
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = 'eu-central-1'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'max-age=86400',
}
# Cache Configuration
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': os.environ.get('REDIS_URL', 'redis://127.0.0.1:6379/1'),
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {'max_connections': 50}
}
}
}
# Celery Configuration
CELERY_BROKER_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
CELERY_RESULT_BACKEND = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC'
# Email Configuration
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.environ.get('EMAIL_HOST')
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = 'noreply@yourdomain.com'
# Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'file': {
'class': 'logging.FileHandler',
'filename': '/var/log/django/app.log',
'formatter': 'verbose'
},
},
'root': {
'handlers': ['console', 'file'],
'level': 'INFO',
},
'loggers': {
'django': {
'handlers': ['console', 'file'],
'level': os.environ.get('DJANGO_LOG_LEVEL', 'INFO'),
'propagate': False,
},
},
}
# Session Configuration
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
5. Gunicorn Configuration
# gunicorn.conf.py
import multiprocessing
bind = "127.0.0.1:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 50
preload_app = True
timeout = 30
keepalive = 2
# Logging
errorlog = "/var/www/django-app/logs/gunicorn_error.log"
accesslog = "/var/www/django-app/logs/gunicorn_access.log"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
# Process naming
proc_name = 'django-app'
# Server mechanics
daemon = False
pidfile = '/var/www/django-app/gunicorn.pid'
user = 'www-data'
group = 'www-data'
tmp_upload_dir = None
# SSL
keyfile = None
certfile = None
6. NGINX Configuration
# Create NGINX site configuration
sudo tee /etc/nginx/sites-available/django-app > /dev/null <<'EOF'
upstream django_backend {
server 127.0.0.1:8000 fail_timeout=0;
}
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL configuration (will be updated by Certbot)
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Basic settings
client_max_body_size 50M;
# Static files
location /static/ {
alias /var/www/django-app/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Media files
location /media/ {
alias /var/www/django-app/media/;
expires 1y;
add_header Cache-Control "public";
}
# Main application
location / {
proxy_pass http://django_backend;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_buffering off;
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Health check
location /health/ {
access_log off;
proxy_pass http://django_backend;
}
}
EOF
# Enable site
sudo ln -s /etc/nginx/sites-available/django-app /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
# Test and reload NGINX
sudo nginx -t
sudo systemctl reload nginx
7. Systemd Service Configuration
# Create systemd service for Django app
sudo tee /etc/systemd/system/django-app.service > /dev/null <<'EOF'
[Unit]
Description=Django App Gunicorn daemon
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/django-app
Environment="PATH=/var/www/django-app/venv/bin"
EnvironmentFile=/var/www/django-app/.env
ExecStart=/var/www/django-app/venv/bin/gunicorn --config gunicorn.conf.py myproject.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
# Create logs directory
sudo mkdir -p /var/www/django-app/logs
sudo chown -R www-data:www-data /var/www/django-app/logs
# Start and enable service
sudo systemctl daemon-reload
sudo systemctl start django-app
sudo systemctl enable django-app
# Check status
sudo systemctl status django-app
8. Celery Workers Setup (Optional)
# Create Celery worker service
sudo tee /etc/systemd/system/celery-worker.service > /dev/null <<'EOF'
[Unit]
Description=Celery Worker Service
After=network.target
[Service]
Type=forking
User=www-data
Group=www-data
WorkingDirectory=/var/www/django-app
Environment="PATH=/var/www/django-app/venv/bin"
EnvironmentFile=/var/www/django-app/.env
ExecStart=/var/www/django-app/venv/bin/celery -A myproject worker --detach --loglevel=info --logfile=/var/www/django-app/logs/celery_worker.log --pidfile=/var/www/django-app/celery_worker.pid
ExecStop=/var/www/django-app/venv/bin/celery -A myproject control shutdown
ExecReload=/bin/kill -s HUP $MAINPID
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
# Create Celery beat service
sudo tee /etc/systemd/system/celery-beat.service > /dev/null <<'EOF'
[Unit]
Description=Celery Beat Service
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/django-app
Environment="PATH=/var/www/django-app/venv/bin"
EnvironmentFile=/var/www/django-app/.env
ExecStart=/var/www/django-app/venv/bin/celery -A myproject beat --loglevel=info --logfile=/var/www/django-app/logs/celery_beat.log --pidfile=/var/www/django-app/celery_beat.pid
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
# Enable and start Celery services
sudo systemctl daemon-reload
sudo systemctl enable celery-worker celery-beat
sudo systemctl start celery-worker celery-beat
Database Configuration and Optimization
PostgreSQL with Connection Pooling
# database.py
import os
from django.db.backends.postgresql import base
from psycopg2 import pool
class PostgreSQLPool:
def __init__(self):
self._pool = None
def initialize_pool(self):
if not self._pool:
self._pool = pool.ThreadedConnectionPool(
minconn=2,
maxconn=20,
host=os.environ.get('DB_HOST'),
database=os.environ.get('DB_NAME'),
user=os.environ.get('DB_USER'),
password=os.environ.get('DB_PASSWORD'),
port=os.environ.get('DB_PORT', 5432)
)
def get_connection(self):
self.initialize_pool()
return self._pool.getconn()
def return_connection(self, connection):
self._pool.putconn(connection)
# Custom database backend
class DatabaseWrapper(base.DatabaseWrapper):
def get_new_connection(self, conn_params):
pool = PostgreSQLPool()
return pool.get_connection()
Database Migrations Best Practices
# management/commands/safe_migrate.py
from django.core.management.base import BaseCommand
from django.core.management import call_command
from django.db import connection
import time
class Command(BaseCommand):
help = 'Safely run database migrations with zero downtime'
def add_arguments(self, parser):
parser.add_argument(
'--check',
action='store_true',
help='Check migrations without applying'
)
def handle(self, *args, **options):
# Check pending migrations
self.stdout.write('Checking pending migrations...')
call_command('showmigrations', '--plan')
if options['check']:
return
# Create backup
self.stdout.write('Creating database backup...')
backup_name = f"backup_{int(time.time())}.sql"
call_command('dbbackup', '--noinput', '--filename', backup_name)
# Apply migrations with safety checks
try:
self.stdout.write('Applying migrations...')
call_command('migrate', '--noinput')
self.stdout.write(self.style.SUCCESS('Migrations completed successfully'))
except Exception as e:
self.stdout.write(self.style.ERROR(f'Migration failed: {e}'))
self.stdout.write('Rolling back to backup...')
call_command('dbrestore', '--noinput', '--filename', backup_name)
raise
Static Files and Media Management
WhiteNoise Configuration
# wsgi.py
import os
from django.core.wsgi import get_wsgi_application
from whitenoise import WhiteNoise
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings.production')
application = get_wsgi_application()
application = WhiteNoise(application, root='/app/staticfiles')
application.add_files('/app/media', prefix='/media/')
S3 Storage for Media Files
# storage_backends.py
from storages.backends.s3boto3 import S3Boto3Storage
from django.conf import settings
import os
class StaticStorage(S3Boto3Storage):
location = 'static'
default_acl = 'public-read'
file_overwrite = False
custom_domain = f'{settings.AWS_S3_CUSTOM_DOMAIN}/static'
class MediaStorage(S3Boto3Storage):
location = 'media'
default_acl = 'private'
file_overwrite = False
custom_domain = f'{settings.AWS_S3_CUSTOM_DOMAIN}/media'
def get_object_parameters(self, name):
params = super().get_object_parameters(name)
# Add security headers
params['Metadata'] = {
'Cache-Control': 'max-age=86400',
'X-Frame-Options': 'SAMEORIGIN',
}
return params
Celery Background Tasks
Celery Configuration
# celery.py
import os
from celery import Celery
from celery.signals import setup_logging
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings.production')
app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
# Configure Celery logging
@setup_logging.connect
def config_loggers(*args, **kwargs):
from logging.config import dictConfig
from django.conf import settings
dictConfig(settings.LOGGING)
# Task routing
app.conf.task_routes = {
'myapp.tasks.send_email': {'queue': 'email'},
'myapp.tasks.generate_report': {'queue': 'reports'},
'myapp.tasks.process_image': {'queue': 'media'},
}
# Rate limiting
app.conf.task_annotations = {
'myapp.tasks.send_email': {'rate_limit': '100/m'},
'myapp.tasks.api_call': {'rate_limit': '30/m'},
}
Example Celery Tasks
# tasks.py
from celery import shared_task
from celery.utils.log import get_task_logger
from django.core.mail import send_mail
from django.core.cache import cache
import time
logger = get_task_logger(__name__)
@shared_task(bind=True, max_retries=3)
def send_email_task(self, subject, message, recipient_list):
try:
send_mail(
subject,
message,
'noreply@yourdomain.com',
recipient_list,
fail_silently=False,
)
logger.info(f"Email sent to {recipient_list}")
return True
except Exception as exc:
logger.error(f"Email sending failed: {exc}")
raise self.retry(exc=exc, countdown=60 * (self.request.retries + 1))
@shared_task
def generate_report(report_id):
"""Generate complex report with progress tracking"""
cache_key = f'report_progress_{report_id}'
# Update progress
for i in range(101):
cache.set(cache_key, i, timeout=3600)
time.sleep(0.1) # Simulate work
if i % 10 == 0:
logger.info(f"Report {report_id}: {i}% complete")
return f"Report {report_id} completed"
@shared_task
def cleanup_old_sessions():
"""Periodic task to clean expired sessions"""
from django.contrib.sessions.models import Session
from django.utils import timezone
expired_sessions = Session.objects.filter(expire_date__lt=timezone.now())
count = expired_sessions.count()
expired_sessions.delete()
logger.info(f"Cleaned up {count} expired sessions")
return count
Caching Strategies
Multi-Level Caching
# caching.py
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from django.core.cache.backends.redis import RedisCache
import hashlib
import json
class SmartCache:
"""Intelligent caching with automatic invalidation"""
@staticmethod
def get_or_set(key, callable, timeout=300, version=None):
# Try to get from cache
value = cache.get(key, version=version)
if value is None:
# Generate value
value = callable()
# Store in cache
cache.set(key, value, timeout, version=version)
return value
@staticmethod
def invalidate_pattern(pattern):
"""Invalidate all cache keys matching pattern"""
if hasattr(cache, '_cache'):
# Redis backend
cache._cache.delete_pattern(f'*{pattern}*')
else:
# Fallback for other backends
cache.clear()
@staticmethod
def cache_key(*args, **kwargs):
"""Generate consistent cache keys"""
key_data = {
'args': args,
'kwargs': kwargs
}
key_string = json.dumps(key_data, sort_keys=True)
return hashlib.md5(key_string.encode()).hexdigest()
# View caching decorator
def smart_cache(timeout=300, key_prefix=None):
def decorator(view_func):
def wrapped_view(request, *args, **kwargs):
# Generate cache key
cache_key = SmartCache.cache_key(
key_prefix or view_func.__name__,
request.path,
request.GET.dict(),
*args,
**kwargs
)
# Check cache
response = cache.get(cache_key)
if response is None:
response = view_func(request, *args, **kwargs)
cache.set(cache_key, response, timeout)
return response
return wrapped_view
return decorator
Django Channels for WebSockets
ASGI Configuration
# asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator
import myapp.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings.production')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(
myapp.routing.websocket_urlpatterns
)
)
),
})
WebSocket Consumer
# consumers.py
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
import json
class NotificationConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.user = self.scope["user"]
self.group_name = f"user_{self.user.id}"
# Join user group
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave user group
await self.channel_layer.group_discard(
self.group_name,
self.channel_name
)
async def receive(self, text_data):
data = json.loads(text_data)
message_type = data.get('type')
if message_type == 'notification_read':
await self.mark_notification_read(data['notification_id'])
@database_sync_to_async
def mark_notification_read(self, notification_id):
from myapp.models import Notification
Notification.objects.filter(
id=notification_id,
user=self.user
).update(is_read=True)
async def send_notification(self, event):
# Send notification to WebSocket
await self.send(text_data=json.dumps({
'type': 'notification',
'message': event['message'],
'timestamp': event['timestamp']
}))
Security Best Practices
Custom Security Middleware
# middleware/security.py
from django.utils.deprecation import MiddlewareMixin
from django.http import HttpResponseForbidden
import hashlib
import hmac
class SecurityMiddleware(MiddlewareMixin):
def process_request(self, request):
# Rate limiting
ip = self.get_client_ip(request)
if self.is_rate_limited(ip):
return HttpResponseForbidden('Rate limit exceeded')
# CSRF validation for API endpoints
if request.path.startswith('/api/'):
if not self.verify_api_signature(request):
return HttpResponseForbidden('Invalid API signature')
# XSS Protection
request.META['HTTP_X_XSS_PROTECTION'] = '1; mode=block'
return None
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def is_rate_limited(self, ip):
from django.core.cache import cache
key = f'rate_limit_{ip}'
requests = cache.get(key, 0)
if requests > 100: # 100 requests per minute
return True
cache.set(key, requests + 1, 60)
return False
def verify_api_signature(self, request):
signature = request.META.get('HTTP_X_API_SIGNATURE')
if not signature:
return False
# Compute expected signature
body = request.body.decode('utf-8')
secret = settings.API_SECRET.encode('utf-8')
expected = hmac.new(secret, body.encode('utf-8'), hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)
Performance Optimization
Database Query Optimization
# optimizations.py
from django.db.models import Prefetch, F, Q
from django.core.cache import cache
class OptimizedQuerySet:
@staticmethod
def get_user_with_related(user_id):
"""Fetch user with all related data in one query"""
from myapp.models import User
return User.objects.select_related(
'profile',
'subscription',
).prefetch_related(
'posts',
'posts__comments',
'posts__comments__author',
Prefetch('followers', queryset=User.objects.only('id', 'username')),
).get(id=user_id)
@staticmethod
def bulk_update_efficiently(updates):
"""Bulk update with single query"""
from myapp.models import Product
products = []
for product_id, data in updates.items():
product = Product(id=product_id, **data)
products.append(product)
Product.objects.bulk_update(
products,
['price', 'stock', 'updated_at'],
batch_size=1000
)
Monitoring and Logging
Custom Monitoring Integration
# monitoring.py
from django.core.management.base import BaseCommand
from django.db import connection
from django.core.cache import cache
import psutil
import logging
logger = logging.getLogger(__name__)
class HealthCheck:
@staticmethod
def check_database():
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
return True, "Database is healthy"
except Exception as e:
return False, str(e)
@staticmethod
def check_cache():
try:
cache.set('health_check', 'ok', 10)
value = cache.get('health_check')
return value == 'ok', "Cache is working"
except Exception as e:
return False, str(e)
@staticmethod
def check_disk_space():
disk_usage = psutil.disk_usage('/')
if disk_usage.percent > 90:
return False, f"Disk usage critical: {disk_usage.percent}%"
return True, f"Disk usage: {disk_usage.percent}%"
@staticmethod
def check_memory():
memory = psutil.virtual_memory()
if memory.percent > 90:
return False, f"Memory usage critical: {memory.percent}%"
return True, f"Memory usage: {memory.percent}%"
# Health check view
from django.http import JsonResponse
def health_check_view(request):
checks = {
'database': HealthCheck.check_database(),
'cache': HealthCheck.check_cache(),
'disk': HealthCheck.check_disk_space(),
'memory': HealthCheck.check_memory(),
}
all_healthy = all(check[0] for check in checks.values())
status_code = 200 if all_healthy else 503
return JsonResponse({
'status': 'healthy' if all_healthy else 'unhealthy',
'checks': {name: {'healthy': check[0], 'message': check[1]}
for name, check in checks.items()}
}, status=status_code)
CI/CD Pipeline for Django
# .github/workflows/django-deploy.yml
name: Deploy Django to CloudPloy
on:
push:
branches: [main]
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'
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install coverage
- name: Run migrations
run: python manage.py migrate
env:
DATABASE_URL: postgres://postgres:postgres@localhost/test_db
- name: Run tests
run: |
coverage run --source='.' manage.py test
coverage xml
env:
DATABASE_URL: postgres://postgres:postgres@localhost/test_db
REDIS_URL: redis://localhost:6379
- name: Upload coverage
uses: codecov/codecov-action@v3
- name: Run linting
run: |
pip install flake8 black isort
flake8 .
black --check .
isort --check-only .
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to CloudPloy
env:
CLOUDPLOY_TOKEN: ${{ secrets.CLOUDPLOY_TOKEN }}
run: |
pip install cloudploy-cli
cloudploy deploy --prod
Common Django Deployment Issues
Issue 1: Static Files Not Loading
# Solution: Configure WhiteNoise properly
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # Add this
'django.contrib.sessions.middleware.SessionMiddleware',
# ... other middleware
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
Issue 2: Database Connection Pool Exhaustion
# Solution: Configure connection pooling
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydb',
'USER': 'myuser',
'PASSWORD': 'mypass',
'HOST': 'localhost',
'PORT': '5432',
'CONN_MAX_AGE': 600, # Persistent connections
'OPTIONS': {
'connect_timeout': 10,
'options': '-c statement_timeout=30000' # 30 second timeout
}
}
}
Issue 3: Memory Leaks in Production
# Solution: Add memory management
# gunicorn_config.py
max_requests = 1000
max_requests_jitter = 50
worker_class = 'sync'
workers = 4
timeout = 30
def worker_int(worker):
"""Log worker timeout"""
worker.log.info("worker received INT or QUIT signal")
def pre_fork(server, worker):
"""Called just before a worker is forked"""
server.log.info("Worker spawned (pid: %s)", worker.pid)
Django Deployment Checklist
✅ Pre-Deployment:
- DEBUG = False
- SECRET_KEY is secure
- ALLOWED_HOSTS configured
- Database migrations tested
- Static files collected
- Security middleware enabled
- SSL certificates ready
- Environment variables set
✅ Performance:
- Database indexes created
- Query optimization complete
- Caching configured
- CDN enabled for static files
- Compression enabled
- Connection pooling setup
✅ Security:
- HTTPS enforced
- CSRF protection enabled
- XSS protection headers
- SQL injection prevention
- Rate limiting configured
- Security headers set
✅ Monitoring:
- Error tracking configured
- Performance monitoring
- Uptime monitoring
- Log aggregation setup
- Alerts configured
9. SSL Certificate Setup
# Install Certbot
sudo apt install -y certbot python3-certbot-nginx
# Get SSL certificate
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Set up auto-renewal
sudo crontab -e
# Add: 0 3 * * * /usr/bin/certbot renew --quiet
10. Final Deployment Steps
# Collect static files
cd /var/www/django-app
source venv/bin/activate
python manage.py collectstatic --noinput
# Run database migrations
python manage.py migrate
# Create superuser (optional)
python manage.py createsuperuser
# Set proper permissions
sudo chown -R www-data:www-data /var/www/django-app
sudo chmod -R 755 /var/www/django-app
Conclusion: Production Django on Ubuntu Server
Deploying Django applications on Ubuntu server provides enterprise-grade performance, security, and control while maintaining cost-effectiveness. With proper configuration of Gunicorn, NGINX, PostgreSQL, and Redis, your Django applications can handle high traffic loads reliably.
What You’ve Accomplished:
✅ Production-ready Django deployment on Ubuntu 24.04 LTS
✅ Gunicorn WSGI server with process management
✅ NGINX reverse proxy with SSL termination
✅ PostgreSQL database with optimization
✅ Redis caching for improved performance
✅ Celery workers for background tasks
✅ Automated SSL certificates with Let’s Encrypt
✅ Monitoring and backup systems
Scaling Your Django Application
Next Steps for Growth:
- Database Replication: PostgreSQL streaming replication for high availability
- Load Balancing: Multiple Django instances behind NGINX load balancer
- Containerization: Docker containers for easier scaling and deployment
- CDN Integration: CloudFlare or AWS CloudFront for static file delivery
- Monitoring Stack: Prometheus + Grafana for comprehensive monitoring
- Caching Strategy: Advanced Redis clustering for large-scale applications
Ubuntu server deployment gives you the foundation for scalable, secure Django applications with complete infrastructure control.
Last updated: September 2025. Django is a registered trademark of the Django Software Foundation.