Python Django VPS Deployment Guide: Gunicorn, systemd & Nginx Setup

A complete production deployment guide for Python Django applications on Linux KVM VPS, covering virtual environments, Gunicorn WSGI sockets, systemd, and Nginx.

Deploying a production Python Django web application on a Linux KVM virtual private server requires configuring an efficient application serving pipeline. The built-in Django development server (runserver) is strictly single-threaded and unsuitable for production traffic. A reliable deployment requires a tiered architecture: Gunicorn as the WSGI application server, systemd for process supervision, and Nginx as the front-end reverse proxy handling SSL termination and static asset delivery.

Production Django Architecture Overview

The production deployment architecture separates responsibilities across three discrete layers:

  1. Application Layer (Django + Gunicorn): Django executes inside an isolated Python virtual environment. Gunicorn forks worker processes that execute Python code, process database queries, and return responses over a local UNIX domain socket.
  2. Supervision Layer (systemd): Manages Gunicorn as a native Linux service, providing automatic restarts on unhandled crashes, system boot orchestration, and centralized logging via journald.
  3. Reverse Proxy Layer (Nginx): Terminates client HTTPS connections, serves static CSS/JavaScript and uploaded media files directly from disk without invoking Python, buffers slow client uploads, and forwards dynamic requests to Gunicorn's socket.

Step 1: Setting Up the Python Virtual Environment and Project

On your Linux KVM VPS, create a dedicated system user and set up the project directory structure:

# Create application user and directory
sudo useradd --system --create-home --shell /bin/bash djangoapp
sudo mkdir -p /var/www/myproject
sudo chown djangoapp:djangoapp /var/www/myproject

# Switch to application user and initialize virtual environment
sudo -u djangoapp bash
cd /var/www/myproject
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install django gunicorn psycopg2-binary

In your Django settings.py, configure the production deployment checklist parameters:

DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com', '127.0.0.1']

STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/myproject/static/'

MEDIA_URL = '/media/'
MEDIA_ROOT = '/var/www/myproject/media/'

Collect static assets into the root static directory:

python manage.py collectstatic --noinput
exit

Step 2: Configuring Gunicorn with systemd Service Supervision

Communicating over a local UNIX domain socket delivers lower latency and reduces networking stack overhead compared to local TCP loopback sockets. Create a systemd service file at /etc/systemd/system/gunicorn.service:

[Unit]
Description=Gunicorn daemon for Django project
After=network.target

[Service]
User=djangoapp
Group=djangoapp
WorkingDirectory=/var/www/myproject
ExecStart=/var/www/myproject/venv/bin/gunicorn \
  --access-logfile - \
  --error-logfile - \
  --workers 3 \
  --bind unix:/run/gunicorn.sock \
  myproject.wsgi:application

Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

As a rule of thumb, configure Gunicorn worker count using the formula (2 * CPU_cores) + 1. On a 2-vCPU VPS instance, 5 workers provide optimal concurrency for I/O-bound web requests.

Enable and start the Gunicorn systemd service:

sudo systemctl daemon-reload
sudo systemctl enable --now gunicorn.service
sudo systemctl status gunicorn.service

Step 3: Configuring Nginx as the Front-End Reverse Proxy

Create an Nginx server block at /etc/nginx/sites-available/myproject to route traffic, serve static files, and proxy dynamic requests:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # Serve static assets directly from disk
    location /static/ {
        alias /var/www/myproject/static/;
        expires 30d;
        access_log off;
    }

    # Serve user uploads directly
    location /media/ {
        alias /var/www/myproject/media/;
        expires 30d;
        access_log off;
    }

    # Proxy dynamic requests to Gunicorn UNIX socket
    location / {
        proxy_pass http://unix:/run/gunicorn.sock;
        proxy_http_version 1.1;

        proxy_set_header Host $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_connect_timeout 60s;
        proxy_read_timeout 120s;
    }
}

Enable the site configuration and test Nginx syntax:

sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Database Connection Pooling and PostgreSQL Tuning

By default, Django closes database connections at the conclusion of every HTTP request. Under high traffic volumes, opening and tearing down SSL/TLS-wrapped TCP connections to PostgreSQL on every request imposes severe CPU overhead and exhausts available database connection limits.

To optimize database connection reuse, configure persistent connections in your settings.py:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'production_db',
        'USER': 'db_user',
        'PASSWORD': 'secure_password',
        'HOST': '127.0.0.1',
        'PORT': '5432',
        'CONN_MAX_AGE': 600,
        'CONN_HEALTH_CHECKS': True,
    }
}

Enabling CONN_MAX_AGE allows Gunicorn worker threads to reuse existing PostgreSQL connections across consecutive requests, slashing database backend latency. Additionally, configure SECURE_SSL_REDIRECT = True, SESSION_COOKIE_SECURE = True, and CSRF_COOKIE_SECURE = True to enforce HTTPS transport integrity.

VPS Resource Sizing for Django Workloads

Python Django applications maintain non-trivial memory baselines due to loaded ORM models and framework dependencies. A typical Gunicorn worker consumes between 80 MB and 150 MB of RAM, depending on caching complexity and query sets. For production applications running a local PostgreSQL database alongside Django, allocate at least 2 vCPUs and 4 GB of RAM to avoid swap thrashing during peak traffic.

StreamData Networks offers high-performance KVM VPS instances equipped with RAID NVMe storage and fast network connectivity across Mumbai, Noida, and Jaipur nodes, providing the ideal hardware foundation for self-hosted Django stacks.

Explore StreamData KVM VPS hosting for scalable cloud instances, review bare-metal servers on StreamData Dedicated Servers, or consult platform features via StreamData Company.


Sources