Deployments August 30, 2025 26 min read

Complete Django Deployment Guide: Deploy Securely on Any Office or Home Server with Nginx, Gunicorn & Cloudflare Tunnel - No Public IP Required

Comprehensive Django deployment guide for office & home servers. Deploy securely behind firewalls using Cloudflare Tunnel - no public IP needed.

A
7AZZANI
63 views

Table of Contents

  • Loading table of contents...

Complete Guide: Deploy Django Applications with Nginx, Gunicorn, and Cloudflare Tunnel

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Project Setup
  4. Gunicorn Configuration
  5. Nginx Setup
  6. Cloudflare Tunnel Configuration
  7. Deployment Workflow
  8. Monitoring and Maintenance
  9. Troubleshooting Common Issues
  10. Best Practices
  11. 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

  1. 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
  1. 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
  1. 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

  1. Database Optimization:
# In settings_production.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'OPTIONS': {
            'MAX_CONNS': 20,
            'conn_max_age': 600,
        }
    }
}
  1. 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',
        }
    }
}
  1. 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

  1. 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
  1. Automated Backups with Cron:
# Edit crontab
crontab -e

# Add daily backup at 2 AM
0 2 * * * /home/yourusername/backup_db.sh

Development Workflow

  1. 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
  1. 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:

  1. Always restart Gunicorn after code changes
  2. Monitor logs regularly to catch issues early
  3. Keep backups of your database and media files
  4. Test locally first before deploying to production
  5. 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

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.

Discussion

Have thoughts or questions about this article? Join the discussion!

Leave a Comment