How to Deploy a Django App on a VPS: Python, Gunicorn, Nginx, and PostgreSQL
Django is one of the most popular Python web frameworks for building production-grade applications — but “it works on my machine” and “it runs reliably in production” are two very different things. Deploying Django to a VPS gives you complete control over your Python version, dependencies, database configuration, and server stack. This guide walks through the complete production deployment: Python virtual environment, Gunicorn WSGI server, Nginx reverse proxy, PostgreSQL database, and automated SSL — everything needed to take a Django project from local development to a live, secure server.
Architecture Overview
The Django production stack has four distinct layers:
- PostgreSQL — production database (SQLite is not suitable for concurrent production traffic)
- Gunicorn — WSGI application server that runs Django and handles Python concurrency
- Nginx — reverse proxy that handles SSL termination, serves static files directly, and proxies dynamic requests to Gunicorn
- Systemd — manages the Gunicorn process, ensuring it starts on boot and restarts on crashes
This architecture separates concerns cleanly: Nginx handles HTTP efficiently, Gunicorn handles Python, and PostgreSQL handles data — each layer doing what it does best.
Prerequisites
- Ubuntu 22.04 LTS VPS with at least 1 GB RAM (2 GB recommended for Django + PostgreSQL)
- A non-root user with sudo privileges and SSH key authentication
- A domain name pointed to your VPS IP
- UFW firewall configured (ports 22, 80, 443 open)
Step 1: Update the System and Install Dependencies
sudo apt update && sudo apt upgrade -y
sudo apt install python3-pip python3-venv python3-dev \
libpq-dev postgresql postgresql-contrib nginx certbot \
python3-certbot-nginx git -y
Step 2: Configure PostgreSQL
sudo systemctl start postgresql
sudo systemctl enable postgresql
sudo -u postgres psql
Inside the PostgreSQL shell:
CREATE DATABASE myproject_db;
CREATE USER myproject_user WITH PASSWORD 'StrongPassword123!';
ALTER ROLE myproject_user SET client_encoding TO 'utf8';
ALTER ROLE myproject_user SET default_transaction_isolation TO 'read committed';
ALTER ROLE myproject_user SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE myproject_db TO myproject_user;
\q
Step 3: Create the Application User and Directory
sudo adduser --system --group --home /var/www/myproject deploy
sudo mkdir -p /var/www/myproject
sudo chown deploy:deploy /var/www/myproject
Step 4: Clone Your Project and Set Up a Virtual Environment
sudo -u deploy git clone https://github.com/yourorg/myproject.git /var/www/myproject/app
cd /var/www/myproject
sudo -u deploy python3 -m venv venv
sudo -u deploy /var/www/myproject/venv/bin/pip install --upgrade pip
sudo -u deploy /var/www/myproject/venv/bin/pip install -r app/requirements.txt
sudo -u deploy /var/www/myproject/venv/bin/pip install gunicorn psycopg2-binary
Step 5: Configure Django for Production
Create a production environment file:
sudo -u deploy nano /var/www/myproject/app/.env
SECRET_KEY=your_very_long_random_secret_key_here
DEBUG=False
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
DATABASE_URL=postgresql://myproject_user:StrongPassword123!@localhost/myproject_db
STATIC_ROOT=/var/www/myproject/staticfiles
MEDIA_ROOT=/var/www/myproject/media
sudo chmod 600 /var/www/myproject/app/.env
In your Django settings.py, configure for production. Key settings:
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'myproject_db',
'USER': 'myproject_user',
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': 'localhost',
'PORT': '5432',
}
}
STATIC_URL = '/static/'
STATIC_ROOT = os.environ.get('STATIC_ROOT', '/var/www/myproject/staticfiles')
MEDIA_URL = '/media/'
MEDIA_ROOT = os.environ.get('MEDIA_ROOT', '/var/www/myproject/media')
# Security settings for production
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
Run migrations and collect static files:
cd /var/www/myproject/app
sudo -u deploy /var/www/myproject/venv/bin/python manage.py migrate
sudo -u deploy /var/www/myproject/venv/bin/python manage.py collectstatic --noinput
sudo -u deploy /var/www/myproject/venv/bin/python manage.py createsuperuser
Step 6: Configure Gunicorn
Test Gunicorn manually first:
cd /var/www/myproject/app
sudo -u deploy /var/www/myproject/venv/bin/gunicorn \
--workers 3 \
--bind unix:/var/www/myproject/gunicorn.sock \
myproject.wsgi:application
Press Ctrl+C to stop. Now create a systemd service for Gunicorn:
sudo nano /etc/systemd/system/gunicorn-myproject.service
[Unit]
Description=Gunicorn daemon for myproject
After=network.target
[Service]
User=deploy
Group=deploy
WorkingDirectory=/var/www/myproject/app
EnvironmentFile=/var/www/myproject/app/.env
ExecStart=/var/www/myproject/venv/bin/gunicorn \
--access-logfile /var/log/gunicorn/myproject-access.log \
--error-logfile /var/log/gunicorn/myproject-error.log \
--workers 3 \
--bind unix:/var/www/myproject/gunicorn.sock \
myproject.wsgi:application
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
sudo mkdir -p /var/log/gunicorn
sudo chown deploy:deploy /var/log/gunicorn
sudo systemctl daemon-reload
sudo systemctl start gunicorn-myproject
sudo systemctl enable gunicorn-myproject
sudo systemctl status gunicorn-myproject
Gunicorn Worker Count
The recommended formula for --workers is (2 × CPU cores) + 1. For a 2 vCPU VPS, use 5 workers. For a 1 vCPU VPS, use 3. Each Gunicorn worker is a separate Python process and consumes approximately 50–100 MB RAM depending on your application’s import footprint.
Step 7: Configure Nginx
sudo nano /etc/nginx/sites-available/myproject
upstream gunicorn_myproject {
server unix:/var/www/myproject/gunicorn.sock fail_timeout=0;
}
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
client_max_body_size 20M;
# Serve static files directly (bypasses Django and Gunicorn entirely)
location /static/ {
alias /var/www/myproject/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location /media/ {
alias /var/www/myproject/media/;
expires 30d;
add_header Cache-Control "public";
}
# Proxy everything else to Gunicorn
location / {
proxy_pass http://gunicorn_myproject;
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_redirect off;
proxy_connect_timeout 60s;
proxy_read_timeout 60s;
}
}
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Step 8: Issue SSL Certificate
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Certbot modifies your Nginx configuration to enable HTTPS and sets up automatic renewal. Verify:
sudo certbot renew --dry-run
Step 9: Set Up Celery for Background Tasks (Optional but Common)
Many Django applications need background task processing — sending emails, processing uploads, running scheduled jobs. Celery with Redis as the message broker is the standard approach:
sudo -u deploy /var/www/myproject/venv/bin/pip install celery redis
Create a systemd service for Celery:
sudo nano /etc/systemd/system/celery-myproject.service
[Unit]
Description=Celery worker for myproject
After=network.target redis.service
[Service]
User=deploy
Group=deploy
WorkingDirectory=/var/www/myproject/app
EnvironmentFile=/var/www/myproject/app/.env
ExecStart=/var/www/myproject/venv/bin/celery \
-A myproject worker \
--loglevel=info \
--logfile=/var/log/celery/myproject-worker.log
Restart=on-failure
[Install]
WantedBy=multi-user.target
sudo mkdir -p /var/log/celery && sudo chown deploy:deploy /var/log/celery
sudo apt install redis-server -y
sudo systemctl enable redis-server
sudo systemctl enable celery-myproject
sudo systemctl start celery-myproject
Step 10: Deployment Updates
Create a deployment script for pushing code updates:
sudo nano /usr/local/bin/deploy-myproject.sh
#!/bin/bash
set -e
APP_DIR=/var/www/myproject/app
VENV=/var/www/myproject/venv
echo "Pulling latest code..."
sudo -u deploy git -C $APP_DIR pull origin main
echo "Installing new dependencies..."
sudo -u deploy $VENV/bin/pip install -r $APP_DIR/requirements.txt -q
echo "Running migrations..."
sudo -u deploy $VENV/bin/python $APP_DIR/manage.py migrate --noinput
echo "Collecting static files..."
sudo -u deploy $VENV/bin/python $APP_DIR/manage.py collectstatic --noinput -v 0
echo "Reloading Gunicorn..."
sudo systemctl reload gunicorn-myproject
echo "Deployment complete."
sudo chmod +x /usr/local/bin/deploy-myproject.sh
Run sudo /usr/local/bin/deploy-myproject.sh for each subsequent deployment. Gunicorn’s reload signal triggers a graceful restart — existing requests complete before workers are replaced, achieving zero-downtime deployments.
Production Checklist
- ☐
DEBUG = Falsein production settings - ☐
SECRET_KEYstored in environment variable, not in code - ☐
ALLOWED_HOSTSrestricted to your domain(s) - ☐ Database uses PostgreSQL, not SQLite
- ☐ Static files served by Nginx, not Django
- ☐ SSL certificate installed and HTTPS redirect active
- ☐
SECURE_SSL_REDIRECTand security headers enabled - ☐ Gunicorn running as a non-root user via systemd
- ☐ Database credentials stored in
.envwith 600 permissions - ☐ Automated backups configured for PostgreSQL
Getting Started
A Django + PostgreSQL deployment works comfortably from a 2 vCPU / 2 GB RAM VPS for most applications, scaling to 4 vCPU / 4 GB RAM for higher-traffic services. Ubuntu VPS plans at VPS.DO provide a clean Ubuntu 22.04 LTS environment with NVMe storage and KVM virtualization — all prerequisites for this guide. The USA data center is ideal for North American Django applications; Hong Kong for Asia-Pacific deployments.
Conclusion
A Django application deployed with Gunicorn, Nginx, PostgreSQL, and systemd service management is production-ready, maintainable, and scalable. The key principles — non-root application user, environment-variable secrets, static files served by Nginx, graceful Gunicorn reloads — apply equally to small side projects and large-scale applications. Once this foundation is in place, adding Celery workers, Redis caching, and CI/CD automation extends it to handle virtually any Django workload.