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
- Create a VPS on DanubeData
- Choose Ubuntu 24.04 LTS
- Select at least the Standard plan (4GB RAM) for production
- 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
- Go to your GitHub repo → Settings → Secrets → Actions
- Add
VPS_HOST: your server IP - 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 = Falsein production - ☑️ Strong
SECRET_KEY(unique, random) - ☑️ Database password is secure
- ☑️ Firewall enabled (UFW)
- ☑️ SSL/TLS enabled (via Caddy)
- ☑️
ALLOWED_HOSTSconfigured - ☑️ 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?
- Create a VPS on DanubeData
- Follow this guide step by step
- 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)
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.