DeployWise
HomeGuidesDeploy Django to VPS
DjangoVPSGunicornNginxPythonUbuntu

Deploy Django to VPS with Gunicorn & Nginx

Deploy a production-ready Django application to any Ubuntu VPS using Gunicorn as the WSGI server, Nginx as a reverse proxy, and free SSL from Let's Encrypt. Covers virtualenv, systemd services, static files, database migrations, and security hardening.

12 min read
Updated March 2026

What you need

A VPS running Ubuntu 22.04 or later (Hetzner, DigitalOcean, Vultr, etc.)
SSH access with root or sudo privileges
Python 3.11+ (we'll install it on the server)
A domain name pointed to your VPS IP address
A Django project in a Git repository
A requirements.txt file in your project root
SpecMinimumRecommended
CPU1 vCPU2 vCPU
RAM1 GB2 GB
Storage20 GB SSD40 GB SSD
Python3.113.12+
Monthly cost~$4/mo~$10/mo

Why deploy Django to a VPS?

Platforms like Heroku, Railway, and Render are convenient for getting started, but costs escalate quickly. Heroku's Basic dyno starts at $7/month per app with limited resources. A $5/month VPS from Hetzner gives you 2 vCPUs, 4 GB RAM, and enough capacity to run multiple Django projects simultaneously.

With a VPS you get full control over your stack: choose your Python version, database, caching layer, and background task runner. You own the infrastructure, and there are no surprise bandwidth or compute charges.

1Initial VPS setup

SSH into your server and update the system packages. Then install Python, pip, venv, and the build tools needed for compiling Python packages with C extensions.

# Connect to your server
ssh root@your-server-ip

# Update package lists and upgrade existing packages
sudo apt update && sudo apt upgrade -y

# Install Python 3, pip, venv, and essential build tools
sudo apt install -y python3 python3-pip python3-venv python3-dev \
  build-essential libpq-dev nginx curl git

# Verify Python version
python3 --version
# Python 3.12.x (or 3.11.x)

# Create a dedicated user for your app (optional but recommended)
sudo adduser --disabled-password --gecos "" deploy
sudo usermod -aG sudo deploy

Using a dedicated non-root user improves security. All following commands assume you are logged in as the deploy user or using sudo.

2Clone project and set up virtualenv

Clone your Django project from GitHub, create an isolated Python virtual environment, and install all dependencies.

# Create a directory for your app
sudo mkdir -p /var/www/myproject
sudo chown deploy:deploy /var/www/myproject
cd /var/www/myproject

# Clone your repository
git clone https://github.com/yourusername/myproject.git .

# Create a virtual environment
python3 -m venv venv

# Activate the virtual environment
source venv/bin/activate

# Upgrade pip and install dependencies
pip install --upgrade pip
pip install -r requirements.txt

# Install Gunicorn if it's not in requirements.txt
pip install gunicorn

Always use a virtual environment in production. It prevents dependency conflicts and makes updates safer.

3Configure Django settings for production

Your Django settings must be hardened for production. Never run with DEBUG=True on a live server. Update your settings.py or create a dedicated settings/production.py:

# settings.py (production overrides)
import os
from dotenv import load_dotenv

load_dotenv()

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
DEBUG = False

ALLOWED_HOSTS = [
    "yourdomain.com",
    "www.yourdomain.com",
    os.environ.get("SERVER_IP", ""),
]

# Database — PostgreSQL recommended for production
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ.get("DB_NAME", "myproject"),
        "USER": os.environ.get("DB_USER", "myproject_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 = os.path.join(BASE_DIR, "staticfiles")

# Media files
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")

# Security settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = "DENY"

Install python-dotenv with pip install python-dotenv to load environment variables from a .env file. Alternatively, use django-environ for a more Django-native approach.

4Collect static files and run migrations

With production settings in place, collect all static assets into STATIC_ROOT and apply database migrations.

# Make sure the virtualenv is activated
cd /var/www/myproject
source venv/bin/activate

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

# Apply database migrations
python manage.py migrate

# Create a superuser for the admin panel
python manage.py createsuperuser

# Quick test — Gunicorn should start without errors
gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
# Visit http://your-server-ip:8000 to verify, then Ctrl+C to stop

Replace myproject.wsgi with your actual Django project name. This is the module path to your wsgi.py file.

5Set up Gunicorn with systemd

Running Gunicorn as a systemd service ensures it starts on boot, restarts on failure, and logs output to the journal. First, create a socket file for Gunicorn:

# /etc/systemd/system/gunicorn.socket
[Unit]
Description=Gunicorn socket for myproject

[Socket]
ListenStream=/run/gunicorn.sock

[Install]
WantedBy=sockets.target

Now create the Gunicorn service file:

# /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn daemon for myproject
Requires=gunicorn.socket
After=network.target

[Service]
User=deploy
Group=www-data
WorkingDirectory=/var/www/myproject
EnvironmentFile=/var/www/myproject/.env
ExecStart=/var/www/myproject/venv/bin/gunicorn \
    --access-logfile - \
    --error-logfile - \
    --workers 3 \
    --bind unix:/run/gunicorn.sock \
    --timeout 120 \
    myproject.wsgi:application
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Enable and start the socket and service:

# Reload systemd to pick up the new files
sudo systemctl daemon-reload

# Enable socket and service to start on boot
sudo systemctl enable gunicorn.socket gunicorn.service

# Start the socket (this will trigger the service on first request)
sudo systemctl start gunicorn.socket

# Check the status
sudo systemctl status gunicorn.socket
sudo systemctl status gunicorn.service

# Test the socket
curl --unix-socket /run/gunicorn.sock localhost
# You should see your Django HTML response

The number of --workers should generally be (2 x CPU cores) + 1. For a 1 vCPU server, 3 workers is a good default.

6Configure Nginx reverse proxy

Nginx sits in front of Gunicorn to handle SSL, serve static files directly, buffer slow clients, and provide HTTP/2 support.

# /etc/nginx/sites-available/myproject
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # Redirect all HTTP to HTTPS (after Certbot setup)
    # return 301 https://$server_name$request_uri;

    client_max_body_size 10M;

    # Serve static files directly — bypass Gunicorn
    location /static/ {
        alias /var/www/myproject/staticfiles/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # Serve media files directly
    location /media/ {
        alias /var/www/myproject/media/;
        expires 7d;
    }

    # Proxy all other requests to Gunicorn
    location / {
        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_pass http://unix:/run/gunicorn.sock;
        proxy_redirect off;
    }
}

Enable the site and test the configuration:

# Create a symlink to enable the site
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/

# Remove the default site (optional)
sudo rm /etc/nginx/sites-enabled/default

# Test the configuration for syntax errors
sudo nginx -t

# Reload Nginx to apply changes
sudo systemctl reload nginx

# Allow HTTP and HTTPS through the firewall
sudo ufw allow 'Nginx Full'

Visit http://yourdomain.com in your browser. You should see your Django application running.

7SSL with Certbot

Install Certbot to obtain a free Let's Encrypt SSL certificate. Certbot will automatically modify your Nginx configuration to handle HTTPS.

# Install Certbot and the Nginx plugin
sudo apt install -y certbot python3-certbot-nginx

# Obtain and install the certificate
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

# Certbot will:
# 1. Verify domain ownership via HTTP challenge
# 2. Download the SSL certificate
# 3. Modify your Nginx config to use HTTPS
# 4. Set up auto-renewal via systemd timer

# Verify auto-renewal is active
sudo systemctl status certbot.timer

# Test renewal (dry run)
sudo certbot renew --dry-run

After Certbot runs, uncomment the return 301 redirect line in your Nginx config to force all traffic to HTTPS. Then run sudo systemctl reload nginx.

8Environment variables with .env

Never hardcode secrets in your codebase. Create a .env file at /var/www/myproject/.env and restrict its permissions:

# /var/www/myproject/.env
DJANGO_SECRET_KEY=your-very-long-random-secret-key-here
DJANGO_SETTINGS_MODULE=myproject.settings
DEBUG=False
SERVER_IP=123.45.67.89

DB_NAME=myproject
DB_USER=myproject_user
DB_PASSWORD=strong-database-password
DB_HOST=localhost
DB_PORT=5432
# Lock down the .env file permissions
chmod 600 /var/www/myproject/.env
chown deploy:deploy /var/www/myproject/.env

# Generate a secure Django secret key
python3 -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

Add .env to your .gitignore so it never gets committed to version control.

Production security checklist

Django provides a built-in deployment checklist. Run python manage.py check --deploy to catch common misconfigurations. Ensure all of the following are set:

DEBUG = FalseNever run with debug enabled in production
SECRET_KEYLoaded from environment variable, not hardcoded
ALLOWED_HOSTSExplicitly list your domain(s) and server IP
SECURE_SSL_REDIRECT = TrueRedirect all HTTP requests to HTTPS
SESSION_COOKIE_SECURE = TrueOnly send session cookies over HTTPS
CSRF_COOKIE_SECURE = TrueOnly send CSRF cookies over HTTPS
SECURE_HSTS_SECONDS = 31536000Tell browsers to always use HTTPS (1 year)
SECURE_HSTS_INCLUDE_SUBDOMAINS = TrueApply HSTS to all subdomains
X_FRAME_OPTIONS = 'DENY'Prevent your site from being embedded in iframes
SECURE_CONTENT_TYPE_NOSNIFF = TruePrevent MIME type sniffing
# Run Django's deployment checklist
python manage.py check --deploy

# You should see: System check identified no issues (0 silenced).

Common errors and fixes

502 Bad Gateway

Nginx can't reach Gunicorn. Check that the Gunicorn service is running: sudo systemctl status gunicorn. Verify the socket file exists: ls -la /run/gunicorn.sock. If it's missing, restart the socket: sudo systemctl restart gunicorn.socket. Also check Gunicorn logs: sudo journalctl -u gunicorn -n 50.

Static files return 404

Make sure you ran python manage.py collectstatic and that STATIC_ROOT matches the alias path in your Nginx config. Verify the directory exists and has correct permissions: ls -la /var/www/myproject/staticfiles/. Nginx must be able to read the files (www-data group).

Migration errors or database connection refused

Verify PostgreSQL is running: sudo systemctl status postgresql. Check your .env database credentials. Ensure the database and user exist: sudo -u postgres psql -c '\l' to list databases. Create them if needed: CREATE DATABASE myproject; CREATE USER myproject_user WITH PASSWORD 'pass'; GRANT ALL ON DATABASE myproject TO myproject_user;

DisallowedHost error

Add your domain and/or server IP to ALLOWED_HOSTS in settings.py. Don't forget both the bare domain and www variant. After changing settings, restart Gunicorn: sudo systemctl restart gunicorn.

Permission denied errors

Check file ownership: chown -R deploy:www-data /var/www/myproject. The deploy user must own the files and www-data group needs read access. The socket directory (/run/) must be writable by the Gunicorn service user.

Skip the manual setup with DeployWise

The steps above work, but they take time and are error-prone. DeployWise automates the entire process. It's open-source and free.

1.Sign in with GitHub and add your VPS server credentials
2.Select your Django repository and configure the project
3.DeployWise automatically detects Django, sets up Gunicorn, configures Nginx, and issues SSL
4.Future deploys happen with one click or automatically on git push
5.Roll back to any previous deployment instantly from the dashboard

DeployWise handles dependency installation, virtualenv creation, Gunicorn systemd service, Nginx config, SSL certificates, and zero-downtime restarts. What takes 30-60 minutes manually is done in under 5 minutes.

Ready to deploy your Django app?

Sign in with GitHub, add your VPS, and have your Django app live in minutes — for free.

Open DeployWise Dashboard

Frequently Asked Questions

How long does it take to deploy Django to a VPS?+

With DeployWise, about 5 minutes. Manually, expect 30-60 minutes for first-time setup including Gunicorn, Nginx, SSL, and database migrations.

Do I need Gunicorn and Nginx for Django in production?+

Yes. Django's built-in runserver is for development only. Gunicorn serves as the WSGI application server while Nginx handles static files, SSL termination, and acts as a reverse proxy.

Which VPS provider is best for Django?+

Hetzner offers the best price-to-performance ratio starting at $4/month. DigitalOcean has excellent Django tutorials. Any provider with Ubuntu 22.04+ and at least 1GB RAM works well.

Can I use PostgreSQL with Django on a VPS?+

Yes, and it's recommended for production. Install PostgreSQL on the same VPS or use a managed database. Configure the DATABASES setting in settings.py with the psycopg2 adapter.

How do I handle Django static files in production?+

Set STATIC_ROOT in settings.py, run python manage.py collectstatic, and configure Nginx to serve the static directory directly. This is much faster than serving static files through Django.

Is Django suitable for high-traffic production sites?+

Absolutely. Instagram, Mozilla, and Pinterest all run on Django. With Gunicorn workers, Nginx, and proper caching, a $10/month VPS can handle thousands of concurrent requests.

How do I update my Django app after deployment?+

Pull the latest code with git pull, install any new dependencies, run collectstatic and migrate, then restart Gunicorn with sudo systemctl restart gunicorn. DeployWise automates this entire process with one click.

Related guides