Complete Guide: Deploy Django Applications with Nginx, Gunicorn, and Cloudflare Tunnel
Table of Contents
- Introduction
- Prerequisites
- Project Setup
- Gunicorn Configuration
- Nginx Setup
- Cloudflare Tunnel Configuration
- Deployment Workflow
- Monitoring and Maintenance
- Troubleshooting Common Issues
- Best Practices
- Automation Scripts
Introduction
Deploying Django applications can be challenging, especially when you want to host them on your own server while maintaining security and performance. This comprehensive guide walks you through deploying a Django application using a powerful combination of:
- Django: Your Python web framework
- Gunicorn: Python WSGI HTTP Server for UNIX
- Nginx: High-performance web server and reverse proxy
- Cloudflare Tunnel: Secure connection between your server and Cloudflare's edge
This setup provides enterprise-level security, performance, and reliability without the need for complex port forwarding or exposing your home server directly to the internet.
Prerequisites
Before starting, ensure you have:
- Ubuntu/Debian-based Linux server (local or cloud)
- Python 3.8 or higher installed
- Git configured on your server
- Basic knowledge of Linux command line
- Cloudflare account (free tier works)
- Domain name managed by Cloudflare
System Requirements
# Minimum server specifications
- RAM: 2GB (4GB recommended)
- Storage: 20GB available space
- Network: Stable internet connection
Project Setup
1. Create Project Directory Structure
# Navigate to your projects directory
cd /home/yourusername/projects
# Clone your Django project (replace with your repository)
git clone https://github.com/yourusername/your-django-app.git
cd your-django-app
# Create and activate virtual environment
python3 -m venv env
source env/bin/activate
2. Install Dependencies
# Upgrade pip
pip install --upgrade pip
# Install project dependencies
pip install -r requirements.txt
# Install production dependencies
pip install gunicorn
3. Configure Django Settings
Create a production settings file:
# Create production settings
cp your_project/settings.py your_project/settings_production.py
Edit settings_production.py
:
# Production settings
DEBUG = False
ALLOWED_HOSTS = ['your-domain.com', 'www.your-domain.com', '127.0.0.1']
# Static files configuration
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# Media files configuration
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Security settings
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
4. Prepare Database and Static Files
# Run database migrations
python manage.py migrate --settings=your_project.settings_production
# Create superuser (optional)
python manage.py createsuperuser --settings=your_project.settings_production
# Collect static files
python manage.py collectstatic --noinput --settings=your_project.settings_production
Gunicorn Configuration
1. Create Gunicorn Configuration File
Create /home/yourusername/projects/your-django-app/gunicorn.conf.py
:
# Gunicorn configuration file
bind = "unix:/home/yourusername/projects/your-django-app/gunicorn.sock"
workers = 3
worker_class = "sync"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 100
timeout = 30
keepalive = 5
preload_app = True
daemon = False
user = "yourusername"
group = "www-data"
tmp_upload_dir = None
errorlog = "/var/log/gunicorn/error.log"
accesslog = "/var/log/gunicorn/access.log"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
2. Create Systemd Service File
Create /etc/systemd/system/gunicorn.service
:
sudo nano /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn daemon for Django Project
After=network.target
[Service]
User=yourusername
Group=www-data
WorkingDirectory=/home/yourusername/projects/your-django-app
ExecStart=/home/yourusername/projects/your-django-app/env/bin/gunicorn \
--config /home/yourusername/projects/your-django-app/gunicorn.conf.py \
your_project.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
3. Configure Gunicorn Logging
# Create log directories
sudo mkdir -p /var/log/gunicorn
sudo chown yourusername:www-data /var/log/gunicorn
sudo chmod 755 /var/log/gunicorn
4. Start and Enable Gunicorn
# Reload systemd daemon
sudo systemctl daemon-reload
# Start Gunicorn service
sudo systemctl start gunicorn
# Enable Gunicorn to start on boot
sudo systemctl enable gunicorn
# Check service status
sudo systemctl status gunicorn
Nginx Setup
1. Install Nginx
# Update package list
sudo apt update
# Install Nginx
sudo apt install nginx
# Start and enable Nginx
sudo systemctl start nginx
sudo systemctl enable nginx
2. Create Nginx Configuration
Create /etc/nginx/sites-available/your-domain.com
:
server {
listen 80;
server_name your-domain.com www.your-domain.com;
# 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;
# Client max body size
client_max_body_size 100M;
# Static files
location /static/ {
alias /home/yourusername/projects/your-django-app/staticfiles/;
expires 30d;
add_header Cache-Control "public, no-transform";
}
# Media files
location /media/ {
alias /home/yourusername/projects/your-django-app/media/;
expires 30d;
add_header Cache-Control "public, no-transform";
}
# Main application
location / {
include proxy_params;
proxy_pass http://unix:/home/yourusername/projects/your-django-app/gunicorn.sock;
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;
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
# Access and error logs
access_log /var/log/nginx/your-domain_access.log;
error_log /var/log/nginx/your-domain_error.log;
}
3. Enable Site Configuration
# Create symbolic link to enable site
sudo ln -s /etc/nginx/sites-available/your-domain.com /etc/nginx/sites-enabled/
# Remove default Nginx configuration
sudo rm /etc/nginx/sites-enabled/default
# Test Nginx configuration
sudo nginx -t
# Restart Nginx
sudo systemctl restart nginx
Cloudflare Tunnel Configuration
1. Install Cloudflared
# Download cloudflared
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
# Install cloudflared
sudo dpkg -i cloudflared-linux-amd64.deb
# Verify installation
cloudflared version
2. Authenticate with Cloudflare
# Login to Cloudflare
cloudflared tunnel login
This will open a browser window. Select your domain and authorize the tunnel.
3. Create Tunnel
# Create a new tunnel
cloudflared tunnel create django-tunnel
# Note down the tunnel ID from the output
4. Create Tunnel Configuration
Create /etc/cloudflared/config.yml
:
tunnel: your-tunnel-id
credentials-file: /home/yourusername/.cloudflared/your-tunnel-id.json
ingress:
- hostname: your-domain.com
service: http://127.0.0.1:80
- hostname: www.your-domain.com
service: http://127.0.0.1:80
- service: http_status:404
5. Create DNS Records
# Create DNS records for your domain
cloudflared tunnel route dns django-tunnel your-domain.com
cloudflared tunnel route dns django-tunnel www.your-domain.com
6. Install Tunnel as Service
# Install tunnel as a system service
sudo cloudflared service install
# Start the service
sudo systemctl start cloudflared
# Enable service on boot
sudo systemctl enable cloudflared
# Check service status
sudo systemctl status cloudflared
Deployment Workflow
Standard Deployment Process
Follow these steps every time you update your application:
# 1. Navigate to project directory
cd /home/yourusername/projects/your-django-app
# 2. Pull latest changes
git pull origin main
# 3. Activate virtual environment
source env/bin/activate
# 4. Update dependencies (if requirements.txt changed)
pip install -r requirements.txt
# 5. Run database migrations (if models changed)
python manage.py migrate --settings=your_project.settings_production
# 6. Collect static files (if static files changed)
python manage.py collectstatic --noinput --settings=your_project.settings_production
# 7. Restart Gunicorn (REQUIRED for code changes)
sudo systemctl restart gunicorn
# 8. Verify deployment
sudo systemctl status gunicorn
curl -I http://127.0.0.1:80
Quick Deployment (One-liner)
For minor updates without database or static file changes:
cd /home/yourusername/projects/your-django-app && git pull && sudo systemctl restart gunicorn
Monitoring and Maintenance
Service Status Monitoring
Create a comprehensive health check script /home/yourusername/health_check.sh
:
#!/bin/bash
echo "🔍 Django Deployment Health Check"
echo "================================="
# Check service status
services=("gunicorn" "nginx" "cloudflared")
for service in "${services[@]}"; do
if systemctl is-active --quiet $service; then
echo "✅ $service is running"
else
echo "❌ $service is NOT running"
fi
done
# Test local connection
echo "🔗 Testing local connection..."
response=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:80)
if [[ $response =~ ^(200|302)$ ]]; then
echo "✅ Local connection successful (HTTP $response)"
else
echo "❌ Local connection failed (HTTP $response)"
fi
# Test domain
echo "🌐 Testing domain connection..."
response=$(curl -s -o /dev/null -w "%{http_code}" https://your-domain.com)
if [[ $response =~ ^(200|302)$ ]]; then
echo "✅ Domain accessible (HTTP $response)"
else
echo "❌ Domain not accessible (HTTP $response)"
fi
echo "================================="
echo "Health check complete!"
Make it executable:
chmod +x /home/yourusername/health_check.sh
Log Monitoring
Real-time Log Monitoring
# Monitor Gunicorn logs (Django application errors)
sudo journalctl -u gunicorn -f
# Monitor Nginx access logs
sudo tail -f /var/log/nginx/your-domain_access.log
# Monitor Nginx error logs
sudo tail -f /var/log/nginx/your-domain_error.log
# Monitor Cloudflare tunnel logs
sudo journalctl -u cloudflared -f
Historical Log Review
# Last 50 Gunicorn entries
sudo journalctl -u gunicorn -n 50
# Last 50 lines of Nginx error log
sudo tail -50 /var/log/nginx/your-domain_error.log
# Last 50 Cloudflare tunnel entries
sudo journalctl -u cloudflared -n 50
Performance Monitoring
Create /home/yourusername/performance_check.sh
:
#!/bin/bash
echo "📊 System Performance Check"
echo "=========================="
# CPU usage
echo "🖥️ CPU Usage:"
top -bn1 | grep "Cpu(s)" | awk '{print $2}' | awk -F'%' '{print " " $1 "%"}'
# Memory usage
echo "💾 Memory Usage:"
free -h | grep "Mem:" | awk '{print " Used: " $3 " / Total: " $2 " (" $3/$2*100 "%)"}'
# Disk usage
echo "💿 Disk Usage:"
df -h / | awk 'NR==2{print " Used: " $3 " / Total: " $2 " (" $5 ")"}'
# Network connections
echo "🌐 Active Connections:"
ss -tuln | grep LISTEN | wc -l | awk '{print " Listening ports: " $1}'
# Process status
echo "⚙️ Service Memory Usage:"
for service in gunicorn nginx cloudflared; do
if pgrep $service > /dev/null; then
memory=$(ps aux | grep $service | grep -v grep | awk '{sum+=$6} END {print sum/1024}')
echo " $service: ${memory}MB"
fi
done
Troubleshooting Common Issues
Issue 1: 502 Bad Gateway Error
Symptoms: Nginx shows 502 error page
Root Causes:
- Gunicorn service not running
- Socket file missing or incorrect permissions
- Gunicorn binding to wrong address
Solutions:
# Check if Gunicorn is running
sudo systemctl status gunicorn
# Restart Gunicorn
sudo systemctl restart gunicorn
# Check socket file exists
ls -la /home/yourusername/projects/your-django-app/gunicorn.sock
# Fix permissions if needed
sudo chown yourusername:www-data /home/yourusername/projects/your-django-app/gunicorn.sock
# Check Gunicorn logs for errors
sudo journalctl -u gunicorn -n 50
# Restart Nginx after fixing Gunicorn
sudo systemctl restart nginx
Issue 2: 522 Connection Timed Out (Cloudflare Error)
Symptoms: Cloudflare error page showing 522 error
Root Causes:
- Cloudflare tunnel not connecting to local service
- Incorrect local address in tunnel configuration
- Nginx not responding on specified port
Solutions:
# Check cloudflared status
sudo systemctl status cloudflared
# Restart cloudflared service
sudo systemctl restart cloudflared
# Verify tunnel configuration
sudo nano /etc/cloudflared/config.yml
# Ensure service points to correct address
# Should be: http://127.0.0.1:80 NOT localhost:80
# Test local connection
curl -I http://127.0.0.1:80
# Check cloudflared logs
sudo journalctl -u cloudflared -n 50
Issue 3: Static Files Not Loading
Symptoms: Website loads but CSS/JS files return 404 errors
Root Causes:
- Static files not collected
- Incorrect Nginx static file configuration
- Wrong file permissions
Solutions:
# Collect static files
cd /home/yourusername/projects/your-django-app
source env/bin/activate
python manage.py collectstatic --noinput --settings=your_project.settings_production
# Check static files directory
ls -la /home/yourusername/projects/your-django-app/staticfiles/
# Verify Nginx configuration
sudo nano /etc/nginx/sites-available/your-domain.com
# Check static files section:
# location /static/ {
# alias /home/yourusername/projects/your-django-app/staticfiles/;
# }
# Fix permissions
sudo chown -R yourusername:www-data /home/yourusername/projects/your-django-app/staticfiles/
sudo chmod -R 644 /home/yourusername/projects/your-django-app/staticfiles/
# Restart Nginx
sudo systemctl restart nginx
Issue 4: Code Changes Not Reflecting
Symptoms: Git pull successful but website shows old version
Root Cause: Gunicorn not restarted after code changes
Solution:
# Always restart Gunicorn after code changes
sudo systemctl restart gunicorn
# For template changes, clear Django cache if configured
python manage.py clear_cache --settings=your_project.settings_production
# Verify Gunicorn restarted successfully
sudo systemctl status gunicorn
Issue 5: Database Migration Errors
Symptoms: 500 server error after model changes
Root Causes:
- Migrations not created or applied
- Database permissions issues
- Migration conflicts
Solutions:
# Navigate to project directory
cd /home/yourusername/projects/your-django-app
source env/bin/activate
# Create migrations
python manage.py makemigrations --settings=your_project.settings_production
# Apply migrations
python manage.py migrate --settings=your_project.settings_production
# Check migration status
python manage.py showmigrations --settings=your_project.settings_production
# Restart Gunicorn
sudo systemctl restart gunicorn
Issue 6: Permission Denied Errors
Symptoms: 500 errors with permission-related messages in logs
Root Causes:
- Incorrect file ownership
- Wrong directory permissions
- Database file permissions (if using SQLite)
Solutions:
# Fix file ownership
sudo chown -R yourusername:www-data /home/yourusername/projects/your-django-app
# Set proper directory permissions
chmod 755 /home/yourusername/projects/your-django-app
# If using SQLite, fix database permissions
chmod 664 /home/yourusername/projects/your-django-app/db.sqlite3
# Fix media directory permissions
sudo chown -R yourusername:www-data /home/yourusername/projects/your-django-app/media/
chmod -R 755 /home/yourusername/projects/your-django-app/media/
# Restart services
sudo systemctl restart gunicorn nginx
Best Practices
Security Best Practices
- Environment Variables: Store sensitive data in environment variables
# Create .env file (never commit to git)
nano /home/yourusername/projects/your-django-app/.env
SECRET_KEY=your-secret-key-here
DATABASE_URL=your-database-url
DEBUG=False
- Firewall Configuration:
# Enable UFW firewall
sudo ufw enable
# Allow SSH
sudo ufw allow ssh
# Allow HTTP and HTTPS (if needed for direct access)
sudo ufw allow 80
sudo ufw allow 443
# Check firewall status
sudo ufw status
- Regular Updates:
# Create update script
nano /home/yourusername/system_update.sh
#!/bin/bash
sudo apt update
sudo apt upgrade -y
sudo apt autoremove -y
sudo systemctl restart gunicorn nginx cloudflared
Performance Optimization
- Database Optimization:
# In settings_production.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'OPTIONS': {
'MAX_CONNS': 20,
'conn_max_age': 600,
}
}
}
- Caching Configuration:
# Redis caching (recommended for production)
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
- Gunicorn Worker Optimization:
# In gunicorn.conf.py - adjust based on your server
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "gevent" # For I/O intensive applications
worker_connections = 1000
Backup Strategy
- Database Backup Script:
# Create backup script
nano /home/yourusername/backup_db.sh
#!/bin/bash
BACKUP_DIR="/home/yourusername/backups"
PROJECT_DIR="/home/yourusername/projects/your-django-app"
DATE=$(date +%Y%m%d_%H%M%S)
# Create backup directory
mkdir -p $BACKUP_DIR
# Backup database (adjust for your database type)
if [ -f "$PROJECT_DIR/db.sqlite3" ]; then
cp "$PROJECT_DIR/db.sqlite3" "$BACKUP_DIR/db_backup_$DATE.sqlite3"
echo "SQLite database backed up: db_backup_$DATE.sqlite3"
fi
# Backup media files
tar -czf "$BACKUP_DIR/media_backup_$DATE.tar.gz" -C "$PROJECT_DIR" media/
echo "Media files backed up: media_backup_$DATE.tar.gz"
# Keep only last 7 days of backups
find $BACKUP_DIR -name "db_backup_*.sqlite3" -mtime +7 -delete
find $BACKUP_DIR -name "media_backup_*.tar.gz" -mtime +7 -delete
- Automated Backups with Cron:
# Edit crontab
crontab -e
# Add daily backup at 2 AM
0 2 * * * /home/yourusername/backup_db.sh
Development Workflow
- Git Hooks for Automated Deployment:
Create .git/hooks/post-receive
:
#!/bin/bash
cd /home/yourusername/projects/your-django-app
git --git-dir=/home/yourusername/projects/your-django-app/.git --work-tree=/home/yourusername/projects/your-django-app checkout -f
source env/bin/activate
pip install -r requirements.txt
python manage.py migrate --settings=your_project.settings_production
python manage.py collectstatic --noinput --settings=your_project.settings_production
sudo systemctl restart gunicorn
- Environment-specific Settings:
# settings/__init__.py
import os
if os.environ.get('DJANGO_ENV') == 'production':
from .production import *
else:
from .development import *
Automation Scripts
Complete Deployment Script
Create /home/yourusername/deploy.sh
:
#!/bin/bash
# Django Deployment Script
PROJECT_DIR="/home/yourusername/projects/your-django-app"
LOG_FILE="/var/log/deployment.log"
# Function to log messages
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a $LOG_FILE
}
log_message "Starting deployment process..."
# Navigate to project directory
cd $PROJECT_DIR || {
log_message "Error: Cannot access project directory"
exit 1
}
# Check for uncommitted changes
if ! git diff-index --quiet HEAD --; then
log_message "Warning: Uncommitted changes detected"
fi
# Pull latest changes
log_message "Pulling latest changes from git..."
if git pull origin main; then
log_message "Git pull successful"
else
log_message "Error: Git pull failed"
exit 1
fi
# Activate virtual environment
log_message "Activating virtual environment..."
source env/bin/activate || {
log_message "Error: Cannot activate virtual environment"
exit 1
}
# Update dependencies
log_message "Updating dependencies..."
pip install -r requirements.txt
# Run migrations
log_message "Running database migrations..."
python manage.py migrate --settings=your_project.settings_production
# Collect static files
log_message "Collecting static files..."
python manage.py collectstatic --noinput --settings=your_project.settings_production
# Run tests (optional)
log_message "Running tests..."
python manage.py test --settings=your_project.settings_production
# Restart Gunicorn
log_message "Restarting Gunicorn..."
if sudo systemctl restart gunicorn; then
log_message "Gunicorn restarted successfully"
else
log_message "Error: Failed to restart Gunicorn"
exit 1
fi
# Wait for service to start
sleep 5
# Health check
log_message "Performing health check..."
if curl -f -s http://127.0.0.1:80 > /dev/null; then
log_message "Deployment successful! Site is responding."
else
log_message "Warning: Site may not be responding properly"
fi
log_message "Deployment process completed"
Make it executable:
chmod +x /home/yourusername/deploy.sh
Service Management Script
Create /home/yourusername/manage_services.sh
:
#!/bin/bash
# Service Management Script
SERVICES=("gunicorn" "nginx" "cloudflared")
case "$1" in
start)
echo "Starting all services..."
for service in "${SERVICES[@]}"; do
echo "Starting $service..."
sudo systemctl start $service
done
;;
stop)
echo "Stopping all services..."
for service in "${SERVICES[@]}"; do
echo "Stopping $service..."
sudo systemctl stop $service
done
;;
restart)
echo "Restarting all services..."
for service in "${SERVICES[@]}"; do
echo "Restarting $service..."
sudo systemctl restart $service
done
;;
status)
echo "Service Status:"
echo "==============="
for service in "${SERVICES[@]}"; do
if systemctl is-active --quiet $service; then
echo "✅ $service is running"
else
echo "❌ $service is stopped"
fi
done
;;
logs)
echo "Recent logs from all services:"
echo "============================="
for service in "${SERVICES[@]}"; do
echo "--- $service logs ---"
sudo journalctl -u $service -n 5 --no-pager
done
;;
*)
echo "Usage: $0 {start|stop|restart|status|logs}"
echo ""
echo "Commands:"
echo " start - Start all services"
echo " stop - Stop all services"
echo " restart - Restart all services"
echo " status - Show status of all services"
echo " logs - Show recent logs from all services"
exit 1
;;
esac
Useful Bash Aliases
Add these to your ~/.bashrc
:
# Django deployment aliases
alias djdeploy='cd /home/yourusername/projects/your-django-app && ./deploy.sh'
alias djlogs='sudo journalctl -u gunicorn -f'
alias djstatus='sudo systemctl status gunicorn nginx cloudflared'
alias djrestart='sudo systemctl restart gunicorn'
alias djhealth='/home/yourusername/health_check.sh'
alias djmanage='/home/yourusername/manage_services.sh'
# Quick navigation
alias djcd='cd /home/yourusername/projects/your-django-app'
alias djenv='cd /home/yourusername/projects/your-django-app && source env/bin/activate'
# Reload aliases
source ~/.bashrc
Conclusion
This comprehensive guide provides everything you need to deploy, monitor, and maintain a Django application using Gunicorn, Nginx, and Cloudflare Tunnel. The setup ensures:
- Security: No direct server exposure, SSL termination at Cloudflare edge
- Performance: Optimized for high traffic with proper caching and compression
- Reliability: Automated health checks and recovery procedures
- Maintainability: Comprehensive logging and monitoring tools
- Scalability: Ready for horizontal scaling when needed
Remember these key points:
- Always restart Gunicorn after code changes
- Monitor logs regularly to catch issues early
- Keep backups of your database and media files
- Test locally first before deploying to production
- Use version control for all configuration files
With this setup, you'll have a production-ready Django deployment that can handle real-world traffic while maintaining security and performance standards.
Advanced Configuration and Optimization
SSL/TLS Configuration
While Cloudflare handles SSL termination, you may want to configure end-to-end encryption:
Generate SSL Certificate for Local Connection
# Create SSL directory
sudo mkdir -p /etc/nginx/ssl
# Generate self-signed certificate (for local connection only)
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/nginx-selfsigned.key \
-out /etc/nginx/ssl/nginx-selfsigned.crt
# Set proper permissions
sudo chmod 600 /etc/nginx/ssl/nginx-selfsigned.key
sudo chmod 644 /etc/nginx/ssl/nginx-selfsigned.crt
Update Nginx Configuration for HTTPS
server {
listen 80;
listen 443 ssl http2;
server_name your-domain.com www.your-domain.com;
# SSL configuration
ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt;
ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Rest of your configuration...
}
Database Configuration for Production
PostgreSQL Setup (Recommended)
# Install PostgreSQL
sudo apt install postgresql postgresql-contrib
# Create database and user
sudo -u postgres psql
CREATE DATABASE your_django_db;
CREATE USER django_user WITH ENCRYPTED PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE your_django_db TO django_user;
ALTER ROLE django_user SET client_encoding TO 'utf8';
ALTER ROLE django_user SET default_transaction_isolation TO 'read committed';
ALTER ROLE django_user SET timezone TO 'UTC';
\q
Django Database Configuration
# In settings_production.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'your_django_db',
'USER': 'django_user',
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': '127.0.0.1',
'PORT': '5432',
'OPTIONS': {
'connect_timeout': 10,
},
'CONN_MAX_AGE': 600,
}
}
Database Backup and Restore Scripts
Create /home/yourusername/scripts/db_backup.sh
:
#!/bin/bash
# PostgreSQL backup script
DB_NAME="your_django_db"
DB_USER="django_user"
BACKUP_DIR="/home/yourusername/backups/database"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
# Create backup directory
mkdir -p $BACKUP_DIR
# Create backup
export PGPASSWORD="$DB_PASSWORD"
pg_dump -h localhost -U $DB_USER -d $DB_NAME > "$BACKUP_DIR/backup_$DATE.sql"
# Compress backup
gzip "$BACKUP_DIR/backup_$DATE.sql"
# Remove old backups
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +$RETENTION_DAYS -delete
echo "Database backup completed: backup_$DATE.sql.gz"
Create /home/yourusername/scripts/db_restore.sh
:
#!/bin/bash
# Database restore script
if [ $# -eq 0 ]; then
echo "Usage: $0 <backup_file.sql.gz>"
echo "Available backups:"
ls -la /home/yourusername/backups/database/
exit 1
fi
BACKUP_FILE="$1"
DB_NAME="your_django_db"
DB_USER="django_user"
# Decompress and restore
export PGPASSWORD="$DB_PASSWORD"
gunzip -c "$BACKUP_FILE" | psql -h localhost -U $DB_USER -d $DB_NAME
echo "Database restore completed from: $BACKUP_FILE"
Redis Configuration for Caching and Sessions
Install and Configure Redis
# Install Redis
sudo apt install redis-server
# Configure Redis
sudo nano /etc/redis/redis.conf
Key Redis configuration changes:
# Bind to localhost only
bind 127.0.0.1
# Set password (uncomment and set)
requirepass your_redis_password
# Memory management
maxmemory 256mb
maxmemory-policy allkeys-lru
# Persistence
save 900 1
save 300 10
save 60 10000
# Restart Redis
sudo systemctl restart redis-server
# Enable Redis on boot
sudo systemctl enable redis-server
Django Redis Configuration
# In settings_production.py
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'PASSWORD': os.environ.get('REDIS_PASSWORD'),
}
}
}
# Use Redis for sessions
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
SESSION_COOKIE_AGE = 86400 # 24 hours
Monitoring and Alerting
System Monitoring with Custom Scripts
Create /home/yourusername/scripts/system_monitor.sh
:
#!/bin/bash
# System monitoring script
LOG_FILE="/var/log/system_monitor.log"
EMAIL="your-email@example.com" # Configure email alerts
THRESHOLD_CPU=80
THRESHOLD_MEMORY=85
THRESHOLD_DISK=90
# Function to send alerts
send_alert() {
local message="$1"
echo "$(date): ALERT - $message" | tee -a $LOG_FILE
# Uncomment to enable email alerts (requires mailutils)
# echo "$message" | mail -s "Server Alert - $(hostname)" $EMAIL
}
# Check CPU usage
cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
if (( $(echo "$cpu_usage > $THRESHOLD_CPU" | bc -l) )); then
send_alert "High CPU usage: ${cpu_usage}%"
fi
# Check memory usage
memory_usage=$(free | grep Mem | awk '{printf("%.0f", $3/$2 * 100.0)}')
if [ "$memory_usage" -gt "$THRESHOLD_MEMORY" ]; then
send_alert "High memory usage: ${memory_usage}%"
fi
# Check disk usage
disk_usage=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
if [ "$disk_usage" -gt "$THRESHOLD_DISK" ]; then
send_alert "High disk usage: ${disk_usage}%"
fi
# Check service status
services=("gunicorn" "nginx" "cloudflared" "postgresql" "redis-server")
for service in "${services[@]}"; do
if ! systemctl is-active --quiet $service; then
send_alert "Service $service is not running"
fi
done
# Check website response
response=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://your-domain.com)
if [ "$response" != "200" ] && [ "$response" != "302" ]; then
send_alert "Website not responding correctly (HTTP $response)"
fi
echo "$(date): System check completed" >> $LOG_FILE
Application Performance Monitoring
Create /home/yourusername/scripts/app_monitor.py
:
#!/usr/bin/env python3
import os
import sys
import django
import logging
from datetime import datetime, timedelta
import json
# Setup Django environment
sys.path.append('/home/yourusername/projects/your-django-app')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project.settings_production')
django.setup()
from django.db import connection
from django.core.management import execute_from_command_line
# Configure logging
logging.basicConfig(
filename='/var/log/app_monitor.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def check_database_performance():
"""Check database query performance"""
try:
with connection.cursor() as cursor:
# Test query performance
start_time = datetime.now()
cursor.execute("SELECT 1;")
query_time = (datetime.now() - start_time).total_seconds()
if query_time > 1.0: # Alert if query takes more than 1 second
logging.warning(f"Slow database query detected: {query_time}s")
else:
logging.info(f"Database query time: {query_time}s")
except Exception as e:
logging.error(f"Database check failed: {e}")
def check_disk_space():
"""Check available disk space"""
import shutil
total, used, free = shutil.disk_usage('/home/yourusername/projects/your-django-app')
free_percent = (free / total) * 100
if free_percent < 10: # Alert if less than 10% free space
logging.warning(f"Low disk space: {free_percent:.1f}% free")
else:
logging.info(f"Disk space: {free_percent:.1f}% free")
def check_log_file_sizes():
"""Check if log files are getting too large"""
log_files = [
'/var/log/nginx/your-domain_access.log',
'/var/log/nginx/your-domain_error.log',
'/var/log/gunicorn/error.log',
'/var/log/gunicorn/access.log'
]
max_size = 100 * 1024 * 1024 # 100MB
for log_file in log_files:
if os.path.exists(log_file):
size = os.path.getsize(log_file)
if size > max_size:
logging.warning(f"Large log file detected: {log_file} ({size/1024/1024:.1f}MB)")
if __name__ == '__main__':
logging.info("Starting application monitoring check")
check_database_performance()
check_disk_space()
check_log_file_sizes()
logging.info("Application monitoring check completed")
Automated Log Rotation
Create /etc/logrotate.d/django-app
:
/var/log/gunicorn/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 644 yourusername yourusername
postrotate
systemctl reload gunicorn
endscript
}
/home/yourusername/projects/your-django-app/logs/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 644 yourusername yourusername
}
Load Balancing and High Availability
Multiple Gunicorn Workers Configuration
Update gunicorn.conf.py
for better performance:
import multiprocessing
# Worker processes
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "gevent" # Async worker for better I/O performance
worker_connections = 1000
# Worker recycling
max_requests = 1000
max_requests_jitter = 50
preload_app = True
# Timeout configuration
timeout = 30
keepalive = 2
graceful_timeout = 30
# Memory management
worker_tmp_dir = "/dev/shm" # Use RAM for temporary files
# Logging
capture_output = True
enable_stdio_inheritance = True
# Process naming
proc_name = "django_app"
# Worker restart configuration
def when_ready(server):
server.log.info("Server is ready. Spawning workers")
def worker_int(worker):
worker.log.info("worker received INT or QUIT signal")
def pre_fork(server, worker):
server.log.info("Worker spawned (pid: %s)", worker.pid)
def post_fork(server, worker):
server.log.info("Worker spawned (pid: %s)", worker.pid)
def post_worker_init(worker):
worker.log.info("Worker initialized (pid: %s)", worker.pid)
Nginx Load Balancing Configuration
For multiple application servers:
upstream django_backend {
least_conn;
server unix:/home/yourusername/projects/your-django-app/gunicorn.sock weight=1 max_fails=3 fail_timeout=30s;
# Add more servers as needed:
# server unix:/path/to/another/gunicorn.sock weight=1 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
listen 80;
server_name your-domain.com www.your-domain.com;
# Connection and request limits
client_max_body_size 100M;
client_body_timeout 60s;
client_header_timeout 60s;
keepalive_timeout 65s;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req zone=api burst=20 nodelay;
# Main application
location / {
proxy_pass http://django_backend;
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;
# Connection pooling
proxy_http_version 1.1;
proxy_set_header Connection "";
# Timeouts
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 8 8k;
proxy_busy_buffers_size 16k;
}
# Health check endpoint
location /health/ {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
}
Container Deployment with Docker
Dockerfile
Create Dockerfile
in your project root:
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DEBIAN_FRONTEND=noninteractive
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
postgresql-client \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy project
COPY . .
# Create non-root user
RUN groupadd -r django && useradd -r -g django django
RUN chown -R django:django /app
# Switch to non-root user
USER django
# Collect static files
RUN python manage.py collectstatic --noinput --settings=your_project.settings_production
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health/ || exit 1
# Run gunicorn
CMD ["gunicorn", "--config", "gunicorn.conf.py", "your_project.wsgi:application"]
Docker Compose Configuration
Create docker-compose.prod.yml
:
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- DJANGO_ENV=production
- DATABASE_URL=postgresql://django_user:${DB_PASSWORD}@db:5432/your_django_db
- REDIS_URL=redis://redis:6379/1
depends_on:
- db
- redis
volumes:
- static_volume:/app/staticfiles
- media_volume:/app/media
restart: unless-stopped
db:
image: postgres:15
environment:
- POSTGRES_DB=your_django_db
- POSTGRES_USER=django_user
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- static_volume:/app/staticfiles
- media_volume:/app/media
- ssl_certs:/etc/nginx/ssl
depends_on:
- web
restart: unless-stopped
volumes:
postgres_data:
redis_data:
static_volume:
media_volume:
ssl_certs:
CI/CD Pipeline with GitHub Actions
Create .github/workflows/deploy.yml
:
name: Deploy to Production
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db
run: |
python manage.py test
- name: Run security checks
run: |
pip install bandit safety
bandit -r . -x tests,env
safety check
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /home/yourusername/projects/your-django-app
git pull origin main
source env/bin/activate
pip install -r requirements.txt
python manage.py migrate --settings=your_project.settings_production
python manage.py collectstatic --noinput --settings=your_project.settings_production
sudo systemctl restart gunicorn
# Health check
sleep 10
curl -f http://127.0.0.1:80/health/ || exit 1
Environment Variables Management
Create Environment Configuration
Create /home/yourusername/projects/your-django-app/.env.production
:
# Django settings
DJANGO_ENV=production
SECRET_KEY=your-very-secure-secret-key-here
DEBUG=False
ALLOWED_HOSTS=your-domain.com,www.your-domain.com
# Database
DATABASE_URL=postgresql://django_user:your_secure_password@localhost:5432/your_django_db
# Redis
REDIS_URL=redis://:your_redis_password@localhost:6379/1
# Email configuration (if using email)
EMAIL_HOST=smtp.your-provider.com
EMAIL_HOST_USER=your-email@your-domain.com
EMAIL_HOST_PASSWORD=your-email-password
EMAIL_PORT=587
EMAIL_USE_TLS=True
# Storage (if using cloud storage)
AWS_ACCESS_KEY_ID=your-aws-access-key
AWS_SECRET_ACCESS_KEY=your-aws-secret-key
AWS_STORAGE_BUCKET_NAME=your-bucket-name
# Sentry (for error tracking)
SENTRY_DSN=your-sentry-dsn-here
# Custom application settings
MAX_UPLOAD_SIZE=104857600 # 100MB
DEFAULT_FROM_EMAIL=noreply@your-domain.com
Load Environment Variables in Django
Update settings_production.py
:
import os
from pathlib import Path
from dotenv import load_dotenv
# Load environment variables
load_dotenv('/home/yourusername/projects/your-django-app/.env.production')
# Security
SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USER'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': os.environ.get('DB_HOST', '127.0.0.1'),
'PORT': os.environ.get('DB_PORT', '5432'),
'CONN_MAX_AGE': 600,
}
}
# Caching
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': os.environ.get('REDIS_URL'),
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
# Email configuration
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.environ.get('EMAIL_HOST')
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS', 'True').lower() == 'true'
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL')
# File upload limits
FILE_UPLOAD_MAX_MEMORY_SIZE = int(os.environ.get('MAX_UPLOAD_SIZE', 104857600))
# Sentry for error tracking
if os.environ.get('SENTRY_DSN'):
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
sentry_sdk.init(
dsn=os.environ.get('SENTRY_DSN'),
integrations=[
DjangoIntegration(auto_enabling=True),
RedisIntegration(),
],
traces_sample_rate=0.1,
send_default_pii=True,
)
Production Deployment Checklist
Pre-Deployment Checklist
-
[ ] Security Settings
- [ ]
DEBUG = False
in production settings - [ ] Strong
SECRET_KEY
configured - [ ]
ALLOWED_HOSTS
properly configured - [ ] HTTPS enforced with security headers
- [ ] Database credentials secured
- [ ]
-
[ ] Environment Configuration
- [ ] Environment variables properly set
- [ ] Database configured and accessible
- [ ] Redis/caching configured
- [ ] Static files serving configured
-
[ ] Code Quality
- [ ] All tests passing
- [ ] Security vulnerabilities checked
- [ ] Code linted and formatted
- [ ] Requirements.txt updated
-
[ ] Infrastructure
- [ ] Server resources adequate
- [ ] Backup strategy implemented
- [ ] Monitoring configured
- [ ] Log rotation configured
Post-Deployment Checklist
-
[ ] Service Status
- [ ] Gunicorn service running
- [ ] Nginx service running
- [ ] Cloudflare tunnel active
- [ ] Database accessible
-
[ ] Functionality Tests
- [ ] Homepage loads correctly
- [ ] Admin interface accessible
- [ ] Static files loading
- [ ] Media files uploading/serving
- [ ] Forms submitting correctly
-
[ ] Performance Tests
- [ ] Page load times acceptable
- [ ] Database queries optimized
- [ ] Caching working correctly
- [ ] SSL certificate valid
-
[ ] Monitoring
- [ ] Error tracking active
- [ ] Log monitoring configured
- [ ] Health checks passing
- [ ] Backup processes running
Regular Maintenance Tasks
Daily Tasks
# Add to crontab: 0 2 * * * /home/yourusername/scripts/daily_maintenance.sh
#!/bin/bash
# Daily maintenance script
# Backup database
/home/yourusername/scripts/db_backup.sh
# Check disk space
df -h / | awk 'NR==2{print strftime("%Y-%m-%d %H:%M:%S") " - Disk usage: " $5}' >> /var/log/maintenance.log
# Check service status
systemctl is-active --quiet gunicorn nginx cloudflared || echo "$(date) - Service check failed" >> /var/log/maintenance.log
# Cleanup old log files
find /var/log -name "*.log.*" -mtime +30 -delete
# Check for security updates
apt list --upgradable | grep -i security >> /var/log/security_updates.log
Weekly Tasks
# Add to crontab: 0 3 * * 0 /home/yourusername/scripts/weekly_maintenance.sh
#!/bin/bash
# Weekly maintenance script
# System updates
apt update
apt list --upgradable
# Database optimization (PostgreSQL)
sudo -u postgres vacuumdb --all --analyze --verbose
# Check SSL certificate expiration
echo | openssl s_client -servername your-domain.com -connect your-domain.com:443 2>/dev/null | openssl x509 -noout -dates
# Generate performance report
/home/yourusername/scripts/performance_report.sh
Monthly Tasks
# Add to crontab: 0 4 1 * * /home/yourusername/scripts/monthly_maintenance.sh
#!/bin/bash
# Monthly maintenance script
# Full system backup
tar -czf "/home/yourusername/backups/full_backup_$(date +%Y%m%d).tar.gz" \
/home/yourusername/projects/your-django-app \
/etc/nginx/sites-available \
/etc/systemd/system/gunicorn.service \
/etc/cloudflared/config.yml
# Security audit
apt install lynis -y
lynis audit system
# Update all packages
apt upgrade -y && apt autoremove -y
# Restart all services
systemctl restart gunicorn nginx cloudflared postgresql redis-server
Additional Resources
- Django Deployment Checklist
- Gunicorn Documentation
- Nginx Configuration Guide
- Cloudflare Tunnel Documentation
- PostgreSQL Performance Tuning
- Redis Best Practices
- Django Security Best Practices
Happy deploying! 🚀
This guide provides a comprehensive foundation for deploying Django applications in production. Remember to adapt configurations based on your specific requirements, traffic patterns, and security needs. Always test changes in a staging environment before applying them to production.