Deployment

Deploying on Linux Servers

Deploying Django applications on Linux servers provides maximum control, performance, and cost-effectiveness. This chapter covers comprehensive server setup, security hardening, service configuration, and deployment automation for Ubuntu, CentOS, and other Linux distributions.

Deploying on Linux Servers

Deploying Django applications on Linux servers provides maximum control, performance, and cost-effectiveness. This chapter covers comprehensive server setup, security hardening, service configuration, and deployment automation for Ubuntu, CentOS, and other Linux distributions.

Server Preparation and Setup

Initial Server Configuration

#!/bin/bash
# scripts/server_setup.sh - Initial server setup script

set -e

# Update system packages
echo "🔄 Updating system packages..."
apt update && apt upgrade -y

# Install essential packages
echo "📦 Installing essential packages..."
apt install -y \
    curl \
    wget \
    git \
    vim \
    htop \
    unzip \
    software-properties-common \
    apt-transport-https \
    ca-certificates \
    gnupg \
    lsb-release \
    fail2ban \
    ufw \
    logrotate \
    supervisor

# Install Python and development tools
echo "🐍 Installing Python and development tools..."
apt install -y \
    python3 \
    python3-pip \
    python3-venv \
    python3-dev \
    build-essential \
    libpq-dev \
    libjpeg-dev \
    libpng-dev \
    libfreetype6-dev \
    libxml2-dev \
    libxslt1-dev \
    libffi-dev \
    libssl-dev

# Install database and cache servers
echo "🗄️ Installing PostgreSQL and Redis..."
apt install -y postgresql postgresql-contrib redis-server

# Install web server
echo "🌐 Installing Nginx..."
apt install -y nginx

# Create application user
echo "👤 Creating application user..."
useradd --system --shell /bin/bash --home /opt/django_app --create-home django
usermod -a -G www-data django

# Set up directory structure
echo "📁 Setting up directory structure..."
mkdir -p /opt/django_app/{logs,static,media,backups}
mkdir -p /var/log/{django,gunicorn,nginx}
chown -R django:django /opt/django_app
chown -R django:django /var/log/django
chown -R django:django /var/log/gunicorn

echo "✅ Server setup completed!"

Security Hardening

#!/bin/bash
# scripts/security_hardening.sh - Security hardening script

set -e

echo "🔒 Starting security hardening..."

# Configure firewall
echo "🔥 Configuring UFW firewall..."
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable

# Configure fail2ban
echo "🛡️ Configuring fail2ban..."
cat > /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
backend = systemd

[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s

[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 6

[nginx-limit-req]
enabled = true
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10

[django-auth]
enabled = true
filter = django-auth
logpath = /var/log/django/django.log
maxretry = 5
EOF

# Create Django fail2ban filter
cat > /etc/fail2ban/filter.d/django-auth.conf << 'EOF'
[Definition]
failregex = ^.*\[.*\] WARNING.*Invalid login attempt.*from <HOST>
ignoreregex =
EOF

# Restart fail2ban
systemctl restart fail2ban
systemctl enable fail2ban

# Configure SSH security
echo "🔑 Hardening SSH configuration..."
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup

cat > /etc/ssh/sshd_config << 'EOF'
Port 22
Protocol 2
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key
HostKey /etc/ssh/ssh_host_ed25519_key

UsePrivilegeSeparation yes
KeyRegenerationInterval 3600
ServerKeyBits 1024

SyslogFacility AUTH
LogLevel INFO

LoginGraceTime 120
PermitRootLogin no
StrictModes yes

RSAAuthentication yes
PubkeyAuthentication yes

IgnoreRhosts yes
RhostsRSAAuthentication no
HostbasedAuthentication no

PermitEmptyPasswords no
ChallengeResponseAuthentication no
PasswordAuthentication no

X11Forwarding no
X11DisplayOffset 10
PrintMotd no
PrintLastLog yes
TCPKeepAlive yes

AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
UsePAM yes

AllowUsers django
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
EOF

systemctl restart sshd

# Set up automatic security updates
echo "🔄 Configuring automatic security updates..."
apt install -y unattended-upgrades
cat > /etc/apt/apt.conf.d/50unattended-upgrades << 'EOF'
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
};

Unattended-Upgrade::Package-Blacklist {
};

Unattended-Upgrade::DevRelease "false";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
EOF

echo "✅ Security hardening completed!"

Database Setup and Configuration

PostgreSQL Configuration

#!/bin/bash
# scripts/setup_postgresql.sh - PostgreSQL setup script

set -e

echo "🗄️ Setting up PostgreSQL..."

# Configure PostgreSQL
sudo -u postgres psql << 'EOF'
CREATE USER django_user WITH PASSWORD 'secure_password_here';
CREATE DATABASE django_app OWNER django_user;
GRANT ALL PRIVILEGES ON DATABASE django_app TO django_user;
ALTER USER django_user CREATEDB;
\q
EOF

# Configure PostgreSQL for production
cat > /etc/postgresql/*/main/postgresql.conf << 'EOF'
# Connection settings
listen_addresses = 'localhost'
port = 5432
max_connections = 100

# Memory settings
shared_buffers = 256MB
effective_cache_size = 1GB
work_mem = 4MB
maintenance_work_mem = 64MB

# Checkpoint settings
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100

# Logging
log_destination = 'stderr'
logging_collector = on
log_directory = 'pg_log'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_rotation_age = 1d
log_rotation_size = 100MB
log_min_duration_statement = 1000
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
log_checkpoints = on
log_connections = on
log_disconnections = on
log_lock_waits = on

# Performance
random_page_cost = 1.1
effective_io_concurrency = 200
EOF

# Configure pg_hba.conf for security
cat > /etc/postgresql/*/main/pg_hba.conf << 'EOF'
# TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             postgres                                peer
local   all             all                                     peer
host    all             all             127.0.0.1/32            md5
host    all             all             ::1/128                 md5
host    django_app      django_user     127.0.0.1/32            md5
EOF

# Restart PostgreSQL
systemctl restart postgresql
systemctl enable postgresql

echo "✅ PostgreSQL setup completed!"

Redis Configuration

#!/bin/bash
# scripts/setup_redis.sh - Redis setup script

set -e

echo "🔴 Setting up Redis..."

# Configure Redis
cat > /etc/redis/redis.conf << 'EOF'
# Network
bind 127.0.0.1
port 6379
timeout 300
tcp-keepalive 60

# General
daemonize yes
supervised systemd
pidfile /var/run/redis/redis-server.pid
loglevel notice
logfile /var/log/redis/redis-server.log
databases 16

# Snapshotting
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis

# Security
requirepass your_redis_password_here
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command DEBUG ""
rename-command CONFIG "CONFIG_b835c3f8a5d9e7f2"

# Memory management
maxmemory 256mb
maxmemory-policy allkeys-lru
maxmemory-samples 5

# Append only file
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# Slow log
slowlog-log-slower-than 10000
slowlog-max-len 128
EOF

# Set proper permissions
chown redis:redis /etc/redis/redis.conf
chmod 640 /etc/redis/redis.conf

# Restart Redis
systemctl restart redis-server
systemctl enable redis-server

echo "✅ Redis setup completed!"

Application Deployment

Deployment Script

#!/bin/bash
# scripts/deploy.sh - Main deployment script

set -e

# Configuration
APP_NAME="django_app"
APP_USER="django"
APP_DIR="/opt/$APP_NAME"
REPO_URL="https://github.com/yourusername/your-django-app.git"
BRANCH="main"
PYTHON_VERSION="3.9"

echo "🚀 Starting deployment of $APP_NAME..."

# Switch to application user
sudo -u $APP_USER bash << EOF
set -e

# Navigate to application directory
cd $APP_DIR

# Backup current version if exists
if [ -d "src" ]; then
    echo "📦 Creating backup of current version..."
    cp -r src src_backup_\$(date +%Y%m%d_%H%M%S)
fi

# Clone or update repository
if [ -d "src" ]; then
    echo "🔄 Updating repository..."
    cd src
    git fetch origin
    git reset --hard origin/$BRANCH
else
    echo "📥 Cloning repository..."
    git clone -b $BRANCH $REPO_URL src
    cd src
fi

# Create virtual environment if it doesn't exist
if [ ! -d "../venv" ]; then
    echo "🐍 Creating virtual environment..."
    python3 -m venv ../venv
fi

# Activate virtual environment
source ../venv/bin/activate

# Upgrade pip
pip install --upgrade pip

# Install dependencies
echo "📦 Installing dependencies..."
pip install -r requirements.txt

# Run Django management commands
echo "🔧 Running Django management commands..."
python manage.py collectstatic --noinput
python manage.py migrate --noinput

# Create superuser if it doesn't exist
python manage.py shell << 'PYTHON'
from django.contrib.auth import get_user_model
User = get_user_model()
if not User.objects.filter(username='admin').exists():
    User.objects.create_superuser('admin', 'admin@example.com', 'secure_password_here')
    print("Superuser created")
else:
    print("Superuser already exists")
PYTHON

# Run deployment checks
python manage.py check --deploy

echo "✅ Application deployment completed!"
EOF

# Restart services
echo "🔄 Restarting services..."
systemctl restart $APP_NAME
systemctl restart nginx

# Run post-deployment checks
echo "🔍 Running post-deployment checks..."
sleep 5

# Check if application is responding
if curl -f -s http://localhost:8000/health/ > /dev/null; then
    echo "✅ Application is responding correctly!"
else
    echo "❌ Application health check failed!"
    exit 1
fi

echo "🎉 Deployment completed successfully!"

Service Configuration

# /etc/systemd/system/django_app.service
[Unit]
Description=Django App Gunicorn daemon
Requires=django_app.socket
After=network.target postgresql.service redis-server.service

[Service]
Type=notify
User=django
Group=django
RuntimeDirectory=gunicorn
WorkingDirectory=/opt/django_app/src
ExecStart=/opt/django_app/venv/bin/gunicorn \
    --config /opt/django_app/gunicorn.conf.py \
    --pid /run/gunicorn/django_app.pid \
    myproject.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
Restart=on-failure
RestartSec=5

# Environment
Environment=DJANGO_SETTINGS_MODULE=myproject.settings.production
EnvironmentFile=/opt/django_app/.env

# Security
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/django_app /var/log/gunicorn /run/gunicorn /tmp

# Resource limits
LimitNOFILE=65536
LimitNPROC=4096

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/django_app.socket
[Unit]
Description=Django App Gunicorn socket

[Socket]
ListenStream=/run/gunicorn/django_app.sock
SocketUser=www-data
SocketMode=600

[Install]
WantedBy=sockets.target

Nginx Configuration

Main Nginx Configuration

# /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024;
    use epoll;
    multi_accept on;
}

http {
    # Basic Settings
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    server_tokens off;
    
    # MIME
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # Logging
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                   '$status $body_bytes_sent "$http_referer" '
                   '"$http_user_agent" "$http_x_forwarded_for" '
                   '$request_time $upstream_response_time';
    
    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log warn;
    
    # Gzip Settings
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml;
    
    # Rate Limiting
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
    limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
    limit_req_zone $binary_remote_addr zone=general:10m rate=200r/m;
    
    # Connection Limiting
    limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
    
    # Buffer Sizes
    client_body_buffer_size 128k;
    client_max_body_size 10m;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 4k;
    
    # Timeouts
    client_body_timeout 12;
    client_header_timeout 12;
    send_timeout 10;
    
    # Include site configurations
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Site-Specific Configuration

# /etc/nginx/sites-available/django_app
upstream django_app {
    server unix:/run/gunicorn/django_app.sock fail_timeout=0;
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

# HTTPS Server
server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;
    
    # SSL Configuration
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
    
    # Modern SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    
    # HSTS
    add_header Strict-Transport-Security "max-age=63072000" always;
    
    # Security Headers
    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';" always;
    
    # Root and index
    root /opt/django_app/src;
    
    # Rate limiting
    limit_req zone=general burst=20 nodelay;
    limit_conn conn_limit_per_ip 10;
    
    # Static files
    location /static/ {
        alias /opt/django_app/staticfiles/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        
        # Compression
        gzip_static on;
        
        # Security
        location ~* \.(py|pyc|pyo|pyd|pdb|sql|sqlite|db)$ {
            deny all;
        }
    }
    
    # Media files
    location /media/ {
        alias /opt/django_app/media/;
        expires 30d;
        add_header Cache-Control "public";
        
        # Security
        location ~* \.(py|pyc|pyo|pyd|pdb|sql|sqlite|db|php|pl|sh)$ {
            deny all;
        }
    }
    
    # Admin rate limiting
    location /admin/login/ {
        limit_req zone=login burst=5 nodelay;
        proxy_pass http://django_app;
        include /etc/nginx/proxy_params;
    }
    
    # API rate limiting
    location /api/ {
        limit_req zone=api burst=50 nodelay;
        proxy_pass http://django_app;
        include /etc/nginx/proxy_params;
    }
    
    # Health check (no rate limiting)
    location /health/ {
        proxy_pass http://django_app;
        include /etc/nginx/proxy_params;
        access_log off;
    }
    
    # Main application
    location / {
        proxy_pass http://django_app;
        include /etc/nginx/proxy_params;
    }
    
    # Error pages
    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;
    
    location = /404.html {
        root /opt/django_app/templates/errors;
        internal;
    }
    
    location = /50x.html {
        root /opt/django_app/templates/errors;
        internal;
    }
}

Proxy Parameters

# /etc/nginx/proxy_params
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Port $server_port;

proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;

proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;

proxy_redirect off;

SSL Certificate Setup

Let's Encrypt with Certbot

#!/bin/bash
# scripts/setup_ssl.sh - SSL certificate setup

set -e

echo "🔒 Setting up SSL certificates..."

# Install Certbot
apt install -y certbot python3-certbot-nginx

# Obtain SSL certificate
certbot --nginx -d yourdomain.com -d www.yourdomain.com \
    --non-interactive \
    --agree-tos \
    --email admin@yourdomain.com \
    --redirect

# Set up automatic renewal
cat > /etc/cron.d/certbot << 'EOF'
0 12 * * * root test -x /usr/bin/certbot -a \! -d /run/systemd/system && perl -e 'sleep int(rand(43200))' && certbot -q renew --nginx
EOF

# Test renewal
certbot renew --dry-run

echo "✅ SSL certificates configured!"

Monitoring and Logging

Log Rotation Configuration

# /etc/logrotate.d/django_app
/var/log/django/*.log {
    daily
    missingok
    rotate 52
    compress
    delaycompress
    notifempty
    create 644 django django
    postrotate
        systemctl reload django_app
    endscript
}

/var/log/gunicorn/*.log {
    daily
    missingok
    rotate 52
    compress
    delaycompress
    notifempty
    create 644 django django
    postrotate
        systemctl reload django_app
    endscript
}

/var/log/nginx/*.log {
    daily
    missingok
    rotate 52
    compress
    delaycompress
    notifempty
    create 644 www-data adm
    prerotate
        if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
            run-parts /etc/logrotate.d/httpd-prerotate; \
        fi
    endscript
    postrotate
        systemctl reload nginx
    endscript
}

System Monitoring Script

#!/bin/bash
# scripts/monitor_system.sh - System monitoring script

# Configuration
ALERT_EMAIL="admin@yourdomain.com"
CPU_THRESHOLD=80
MEMORY_THRESHOLD=80
DISK_THRESHOLD=85

# Check CPU usage
CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | awk -F'%' '{print $1}')
if (( $(echo "$CPU_USAGE > $CPU_THRESHOLD" | bc -l) )); then
    echo "High CPU usage: $CPU_USAGE%" | mail -s "Server Alert: High CPU" $ALERT_EMAIL
fi

# Check memory usage
MEMORY_USAGE=$(free | grep Mem | awk '{printf("%.1f", $3/$2 * 100.0)}')
if (( $(echo "$MEMORY_USAGE > $MEMORY_THRESHOLD" | bc -l) )); then
    echo "High memory usage: $MEMORY_USAGE%" | mail -s "Server Alert: High Memory" $ALERT_EMAIL
fi

# Check disk usage
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ $DISK_USAGE -gt $DISK_THRESHOLD ]; then
    echo "High disk usage: $DISK_USAGE%" | mail -s "Server Alert: High Disk Usage" $ALERT_EMAIL
fi

# Check if Django app is running
if ! systemctl is-active --quiet django_app; then
    echo "Django application is not running!" | mail -s "Server Alert: Django App Down" $ALERT_EMAIL
fi

# Check if Nginx is running
if ! systemctl is-active --quiet nginx; then
    echo "Nginx is not running!" | mail -s "Server Alert: Nginx Down" $ALERT_EMAIL
fi

# Check database connectivity
if ! sudo -u django psql -d django_app -c "SELECT 1;" > /dev/null 2>&1; then
    echo "Database connection failed!" | mail -s "Server Alert: Database Connection Failed" $ALERT_EMAIL
fi

This comprehensive Linux server deployment guide provides everything needed to deploy Django applications securely and efficiently on Linux servers.