BlogTutorialsDeploy Django to VPS: Complete Production Guide (2025)

Deploy Django to VPS: Complete Production Guide (2025)

Adrian Silaghi
Adrian Silaghi
January 17, 2026
20 min read
8 views
#django #python #vps #deployment #gunicorn #nginx #caddy #postgresql #production #github actions
Deploy Django to VPS: Complete Production Guide (2025)

Django is one of the most popular Python web frameworks, powering sites like Instagram, Pinterest, and Disqus. But deploying Django to production is notoriously tricky—there's no "one-click deploy" like with some platforms.

This guide walks you through deploying a Django application to a VPS from scratch, covering everything from server setup to automated deployments. By the end, you'll have a production-ready Django application with:

  • Gunicorn as the WSGI server
  • Nginx as the reverse proxy
  • PostgreSQL database
  • SSL/TLS certificates (automatic with Caddy)
  • Systemd process management
  • Automated deployments with GitHub Actions

Prerequisites

  • A Django project ready for deployment
  • A VPS with Ubuntu 22.04/24.04 LTS
  • A domain name pointed to your server
  • Basic Linux command line knowledge

Recommended VPS Specs

App Size Expected Traffic Recommended DanubeData Plan
Small < 10K requests/day 2 vCPU, 2GB RAM Starter (€4.49/mo)
Medium 10K-100K requests/day 2 vCPU, 4GB RAM Standard (€8.99/mo)
Large 100K+ requests/day 4 vCPU, 8GB RAM Performance (€17.99/mo)

Step 1: Provision Your VPS

  1. Create a VPS on DanubeData
  2. Choose Ubuntu 24.04 LTS
  3. Select at least the Standard plan (4GB RAM) for production
  4. Add your SSH key for secure access

Step 2: Initial Server Setup

# SSH into your server
ssh root@YOUR_SERVER_IP

# Update system packages
apt update && apt upgrade -y

# Set timezone
timedatectl set-timezone Europe/Berlin

# Create a deploy user (don't run apps as root!)
adduser deploy
usermod -aG sudo deploy

# Copy SSH keys to deploy user
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

# Test login as deploy user
su - deploy
exit

# Configure firewall
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Step 3: Install Python and Dependencies

# Install Python 3.12 and pip
apt install -y python3.12 python3.12-venv python3.12-dev python3-pip

# Install system dependencies for common Python packages
apt install -y build-essential libpq-dev libffi-dev libssl-dev

# Install PostgreSQL
apt install -y postgresql postgresql-contrib

# Install Caddy (simpler than Nginx, auto-SSL)
apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update
apt install -y caddy

# Install Git
apt install -y git

Step 4: Set Up PostgreSQL Database

# Switch to postgres user
sudo -u postgres psql

-- Create database and user
CREATE DATABASE myapp_db;
CREATE USER myapp_user WITH PASSWORD 'your_secure_password_here';

-- Configure for Django
ALTER ROLE myapp_user SET client_encoding TO 'utf8';
ALTER ROLE myapp_user SET default_transaction_isolation TO 'read committed';
ALTER ROLE myapp_user SET timezone TO 'UTC';

-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE myapp_db TO myapp_user;

-- Exit
q

Step 5: Deploy Your Django Application

# Switch to deploy user
su - deploy

# Create application directory
mkdir -p ~/apps
cd ~/apps

# Clone your repository
git clone https://github.com/yourusername/your-django-app.git myapp
cd myapp

# Create virtual environment
python3.12 -m venv venv
source venv/bin/activate

# Install dependencies
pip install --upgrade pip
pip install -r requirements.txt

# Install Gunicorn
pip install gunicorn

Step 6: Configure Django for Production

Create Production Settings

# myapp/settings/production.py (or modify settings.py)

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

# Security settings
DEBUG = False
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',')

# Database
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME', 'myapp_db'),
        'USER': os.environ.get('DB_USER', 'myapp_user'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST', 'localhost'),
        'PORT': os.environ.get('DB_PORT', '5432'),
    }
}

# Static files
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'

# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# Security
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True

# HTTPS redirect (handled by Caddy, but good to have)
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

# Logging
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'WARNING',
            'class': 'logging.FileHandler',
            'filename': BASE_DIR / 'logs' / 'django.log',
        },
    },
    'root': {
        'handlers': ['file'],
        'level': 'WARNING',
    },
}

Create Environment File

# Create environment file
cat > /home/deploy/apps/myapp/.env << 'EOF'
DJANGO_SECRET_KEY=your-super-secret-key-here-generate-a-new-one
DJANGO_ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
DJANGO_SETTINGS_MODULE=myapp.settings.production

DB_NAME=myapp_db
DB_USER=myapp_user
DB_PASSWORD=your_secure_password_here
DB_HOST=localhost
DB_PORT=5432
EOF

# Secure the file
chmod 600 /home/deploy/apps/myapp/.env

Generate a Secure Secret Key

# Generate secret key
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

Step 7: Run Django Setup Commands

# Load environment variables
set -a; source .env; set +a

# Create logs directory
mkdir -p logs

# Run migrations
python manage.py migrate

# Collect static files
python manage.py collectstatic --noinput

# Create superuser (optional)
python manage.py createsuperuser

# Test the app runs
python manage.py runserver 0.0.0.0:8000
# Visit http://YOUR_IP:8000 to test, then Ctrl+C to stop

Step 8: Configure Gunicorn

Create Gunicorn Config

# /home/deploy/apps/myapp/gunicorn.conf.py
import multiprocessing

# Bind to localhost (Caddy will proxy)
bind = "127.0.0.1:8000"

# Workers = (2 x CPU cores) + 1
workers = multiprocessing.cpu_count() * 2 + 1

# Worker class
worker_class = "sync"  # Use "gevent" for async if needed

# Timeout
timeout = 30
graceful_timeout = 30

# Logging
accesslog = "/home/deploy/apps/myapp/logs/gunicorn-access.log"
errorlog = "/home/deploy/apps/myapp/logs/gunicorn-error.log"
loglevel = "warning"

# Process naming
proc_name = "myapp"

# Reload on code changes (disable in production normally)
reload = False

Create Systemd Service

# /etc/systemd/system/myapp.service
sudo tee /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=Gunicorn daemon for Django myapp
After=network.target postgresql.service

[Service]
User=deploy
Group=deploy
WorkingDirectory=/home/deploy/apps/myapp
EnvironmentFile=/home/deploy/apps/myapp/.env
ExecStart=/home/deploy/apps/myapp/venv/bin/gunicorn 
    --config /home/deploy/apps/myapp/gunicorn.conf.py 
    myapp.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp

# Check status
sudo systemctl status myapp

Step 9: Configure Caddy (Reverse Proxy + SSL)

Caddy automatically handles SSL certificates via Let's Encrypt:

# /etc/caddy/Caddyfile
sudo tee /etc/caddy/Caddyfile << 'EOF'
yourdomain.com {
    # Proxy to Gunicorn
    reverse_proxy localhost:8000

    # Serve static files directly
    handle_path /static/* {
        root * /home/deploy/apps/myapp/staticfiles
        file_server
    }

    # Serve media files directly
    handle_path /media/* {
        root * /home/deploy/apps/myapp/media
        file_server
    }

    # Security headers
    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy strict-origin-when-cross-origin
        -Server
    }

    # Gzip compression
    encode gzip

    # Logging
    log {
        output file /var/log/caddy/access.log
    }
}

# Redirect www to non-www
www.yourdomain.com {
    redir https://yourdomain.com{uri} permanent
}
EOF

# Create log directory
sudo mkdir -p /var/log/caddy
sudo chown caddy:caddy /var/log/caddy

# Reload Caddy
sudo systemctl reload caddy

# Check status
sudo systemctl status caddy

Step 10: Set Up Celery (Optional - Background Tasks)

If your Django app uses Celery for background tasks:

# Install Redis (message broker)
sudo apt install -y redis-server
sudo systemctl enable redis-server

# Add to requirements.txt
# celery
# redis

# Create Celery systemd service
sudo tee /etc/systemd/system/myapp-celery.service << 'EOF'
[Unit]
Description=Celery Worker for myapp
After=network.target redis.service

[Service]
User=deploy
Group=deploy
WorkingDirectory=/home/deploy/apps/myapp
EnvironmentFile=/home/deploy/apps/myapp/.env
ExecStart=/home/deploy/apps/myapp/venv/bin/celery -A myapp worker -l warning
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable myapp-celery
sudo systemctl start myapp-celery

Step 11: Automated Deployment with GitHub Actions

# .github/workflows/deploy.yml
name: Deploy Django to VPS

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Deploy to VPS
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.VPS_HOST }}
          username: deploy
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            cd /home/deploy/apps/myapp

            # Pull latest code
            git pull origin main

            # Activate virtual environment
            source venv/bin/activate

            # Install dependencies
            pip install -r requirements.txt

            # Load environment
            set -a; source .env; set +a

            # Run migrations
            python manage.py migrate --noinput

            # Collect static files
            python manage.py collectstatic --noinput

            # Restart Gunicorn
            sudo systemctl restart myapp

            # Restart Celery (if using)
            # sudo systemctl restart myapp-celery

            echo "Deployment complete!"

Set Up GitHub Secrets

  1. Go to your GitHub repo → Settings → Secrets → Actions
  2. Add VPS_HOST: your server IP
  3. Add VPS_SSH_KEY: your private SSH key

Allow deploy user to restart services without password

# Add to sudoers
sudo visudo

# Add this line at the end:
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp, /bin/systemctl restart myapp-celery

Step 12: Monitoring and Maintenance

Check Logs

# Django/Gunicorn logs
tail -f /home/deploy/apps/myapp/logs/gunicorn-error.log
tail -f /home/deploy/apps/myapp/logs/django.log

# Caddy logs
sudo tail -f /var/log/caddy/access.log

# Systemd service logs
sudo journalctl -u myapp -f
sudo journalctl -u myapp-celery -f

Useful Management Commands

# Restart application
sudo systemctl restart myapp

# View service status
sudo systemctl status myapp

# Check memory usage
free -h

# Check disk usage
df -h

# View running processes
htop

Database Backups

# Create backup script
cat > /home/deploy/backup-db.sh << 'EOF'
#!/bin/bash
BACKUP_DIR="/home/deploy/backups"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
mkdir -p $BACKUP_DIR

pg_dump -U myapp_user myapp_db | gzip > "$BACKUP_DIR/myapp_db_$DATE.sql.gz"

# Keep only last 7 days
find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete

# Optional: Upload to S3
# rclone copy "$BACKUP_DIR/myapp_db_$DATE.sql.gz" danubedata:backups/django/
EOF

chmod +x /home/deploy/backup-db.sh

# Add to crontab (daily at 3 AM)
(crontab -l 2>/dev/null; echo "0 3 * * * /home/deploy/backup-db.sh") | crontab -

Security Checklist

  • ☑️ DEBUG = False in production
  • ☑️ Strong SECRET_KEY (unique, random)
  • ☑️ Database password is secure
  • ☑️ Firewall enabled (UFW)
  • ☑️ SSL/TLS enabled (via Caddy)
  • ☑️ ALLOWED_HOSTS configured
  • ☑️ Security headers set
  • ☑️ Regular backups configured
  • ☑️ SSH key authentication (no password login)
  • ☑️ Updates applied regularly

Troubleshooting

502 Bad Gateway

# Check if Gunicorn is running
sudo systemctl status myapp

# Check Gunicorn logs
tail -f /home/deploy/apps/myapp/logs/gunicorn-error.log

# Common fix: restart the service
sudo systemctl restart myapp

Static Files Not Loading

# Ensure collectstatic was run
python manage.py collectstatic --noinput

# Check Caddy config paths
# Ensure /home/deploy/apps/myapp/staticfiles exists

Database Connection Errors

# Check PostgreSQL is running
sudo systemctl status postgresql

# Verify credentials
psql -U myapp_user -d myapp_db -h localhost

Cost Comparison

Platform Cost/Month Notes
Heroku $25-50+ Dyno + database add-on
Railway $20-50+ Usage-based, can spike
Render $25-50+ Web service + database
DanubeData VPS €8.99 All-in-one, including database

Get Started Today

Ready to deploy your Django application to production?

  1. Create a VPS on DanubeData
  2. Follow this guide step by step
  3. Your Django app will be live in under an hour

DanubeData VPS for Django:

  • €8.99/mo Standard plan (4GB RAM) - perfect for most Django apps
  • Pre-installed Ubuntu 24.04 LTS
  • NVMe storage for fast database queries
  • 20TB included bandwidth
  • German data center (GDPR compliant)

👉 Create Your Django VPS

Or pair with a Managed PostgreSQL Database if you prefer not to manage the database yourself.

Need help deploying Django? Contact our team—we're Python developers too.

Share this article

Ready to Get Started?

Deploy your infrastructure in minutes with DanubeData's managed services.