How to Deploy a Django App on a VPS: Python, Gunicorn, Nginx, and PostgreSQL

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:

  1. PostgreSQL — production database (SQLite is not suitable for concurrent production traffic)
  2. Gunicorn — WSGI application server that runs Django and handles Python concurrency
  3. Nginx — reverse proxy that handles SSL termination, serves static files directly, and proxies dynamic requests to Gunicorn
  4. 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 = False in production settings
  • SECRET_KEY stored in environment variable, not in code
  • ALLOWED_HOSTS restricted 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_REDIRECT and security headers enabled
  • ☐ Gunicorn running as a non-root user via systemd
  • ☐ Database credentials stored in .env with 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.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!