Deploy Django on Vultr VPS with Gunicorn & Nginx (2026)

This post contains affiliate links. If you purchase through our links, we may earn a commission at no extra cost to you.

Deploy Django on Vultr VPS with Gunicorn & Nginx (2026)

Vultr’s own Django documentation targets Ubuntu 14.04 — a release that’s been end-of-life since 2019. Every other guide out there uses DigitalOcean as the example host and skips either the UFW rules, the SSL setup, or the production settings checklist. This guide covers the full stack on Ubuntu 24.04 LTS: Vultr provisioning, Python virtual environment, Gunicorn with systemd socket activation, Nginx as a reverse proxy, PostgreSQL, and Let’s Encrypt SSL via Certbot. When you’re done, you’ll have a production-grade Django app running — not a dev server that breaks the moment you add a second user.

Architecture diagram showing request flow from browser → Nginx → Gunicorn socket → Django app → PostgreSQL database on a Vultr VPS
Architecture diagram showing request flow from browser → Nginx → Gunicorn socket → Django app → PostgreSQL database on a Vultr VPS
🛠️ Free tools for this guide: Systemd Service Generator (generate the .service unit that keeps your Django app running)

Choosing Your Vultr Plan for Django

Before you spin up a server, pick the right size. The $6/month High Performance plan (1 vCPU, 1 GB RAM, 25 GB NVMe SSD) is the minimum viable option for Django + Gunicorn + Nginx + PostgreSQL on a single VPS. In practice, with 3 Gunicorn workers (~150 MB), PostgreSQL (~75 MB), Nginx (~15 MB), and OS overhead, you’re already using 40–50% of that 1 GB. It works for a personal project or low-traffic side app, but it’ll struggle the moment you add background tasks or hit a few hundred concurrent users.

The $12/month plan (1 vCPU, 2 GB RAM) is the realistic starting point for anything you’re calling “production.” I’ve been running a Django app on Vultr’s High Performance tier for months now, and the NVMe SSD makes a genuine difference for PostgreSQL — random read IOPS are noticeably better than what you’d get from a SATA-based cloud instance at the same price.

vultr

Plan Price vCPU RAM Storage Gunicorn Workers
High Performance $6/mo 1 1 GB 25 GB NVMe 3 (starter/test)
High Performance $12/mo 1 2 GB 55 GB NVMe 3–5 (production)
High Performance $24/mo 2 4 GB 80 GB NVMe 5–9 (scale)

Prices last checked: 2026-06-21

Worker count formula: (2 × CPU cores) + 1. A 1 vCPU server gets 3 workers; a 2 vCPU server gets 5. Each worker is a separate Python process handling one request at a time — the right count lets workers sit idle waiting for DB queries without starving the CPU.

If you want to understand how Vultr compares to the alternatives before committing, the our Vultr review and Vultr pricing breakdown cover the full picture. The short version: Vultr wins on raw compute price and NVMe storage; you just do more of your own ops work than you would on a managed platform.

Step 1: Provision the Vultr Server

Log in to the Vultr console and go to Deploy → Cloud Compute. Choose:

  • Plan: High Performance or High Frequency (both use NVMe)
  • OS: Ubuntu 24.04 LTS (Noble Numbat) — not 22.04, and definitely not anything older
  • Location: Nearest datacenter to your primary user base (Vultr has 32 locations)
  • Optional: Add your SSH public key during setup to skip password auth entirely

Deploy the instance and note the IP address. SSH in as root:

ssh root@YOUR_SERVER_IP

Step 2: Initial Server Setup

Run a full system update first — you want Ubuntu 24.04’s packages, not whatever shipped with the base image:

apt update && apt upgrade -y

Create a non-root deploy user. Running Gunicorn as root is a security problem you don’t want:

adduser django
gpasswd -a django sudo
su - django

Set up the firewall before anything else. Allow SSH, then enable UFW:

sudo ufw allow OpenSSH
sudo ufw enable

💡 Tip: Prefer a visual approach? You can build these rules with our free Firewall Rule Builder — it generates ufw and iptables commands right in your browser.

You’ll add Nginx rules later after it’s installed. Don’t skip the firewall step — a fresh VPS is exposed to the internet from the moment it spins up.

Step 3: Install System Dependencies

One command installs everything you’ll need:

sudo apt install python3 python3-venv python3-pip python3-dev \
    build-essential libpq-dev postgresql postgresql-contrib nginx curl -y

Why each package matters:

  • python3-dev + build-essential — required to compile psycopg2 (PostgreSQL adapter) from source
  • libpq-dev — PostgreSQL client libraries; psycopg2 won’t build without this
  • postgresql, postgresql-contrib — database server running locally
  • nginx — reverse proxy that handles incoming HTTP/HTTPS and passes requests to Gunicorn

Ubuntu 24.04 ships with Python 3.12 by default. No PPA or manual Python install needed.

Step 4: Configure PostgreSQL

Never use SQLite in a Django app served by Gunicorn. With multiple workers, SQLite’s file locking triggers “database is locked” errors under concurrent writes — it’s not an edge case, it’s the default behavior of multi-worker WSGI servers. PostgreSQL handles concurrent connections cleanly.

Switch to the postgres user and open the psql shell:

sudo -u postgres psql

Run these commands inside psql:

CREATE DATABASE myproject;
CREATE USER myprojectuser WITH PASSWORD 'strong_password_here';
ALTER ROLE myprojectuser SET client_encoding TO 'utf8';
ALTER ROLE myprojectuser SET default_transaction_isolation TO 'read committed';
ALTER ROLE myprojectuser SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE myproject TO myprojectuser;
\q

Replace myproject, myprojectuser, and strong_password_here with real values. Keep the password somewhere secure — you’ll need it in your .env file shortly.

Step 5: Set Up the Python Virtual Environment

Put your Django app under /var/www/ rather than your home directory. Nginx runs as www-data and needs to traverse the path to read static files — home directories often block this:

sudo mkdir -p /var/www/django_app
sudo chown -R $USER:$USER /var/www/django_app
cd /var/www/django_app

python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install django gunicorn psycopg2-binary

If you’re deploying an existing project, clone it instead:

git clone https://github.com/youruser/yourproject.git .
pip install -r requirements.txt

Note: psycopg2-binary is fine for development and most production deployments. If you’re compiling for a specific architecture or have specific performance requirements, install psycopg2 (without -binary) and let it compile against the system libpq-dev.

Step 6: Django Production Settings

This is where most deployments go wrong. The production settings checklist isn’t optional — Django’s dev defaults will get you hacked or break your app under real traffic.

Create a .env file at /var/www/django_app/.env:

SECRET_KEY=your-50-plus-character-random-key-here
DB_PASSWORD=strong_password_here

Then update settings.py (or a separate settings/production.py):

import os

# SECURITY — never hardcode these in production
SECRET_KEY = os.environ['SECRET_KEY']
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com', 'YOUR_SERVER_IP']

# Database — PostgreSQL only
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'myproject',
        'USER': 'myprojectuser',
        'PASSWORD': os.environ['DB_PASSWORD'],
        'HOST': 'localhost',
        'PORT': '',
        'CONN_MAX_AGE': 60,  # persistent connections
    }
}

# Static and media files
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')

# HTTPS security (enable after SSL is configured)
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True

Key things to get right:

Setting Dev Default Required for Production
DEBUG True False — mandatory
SECRET_KEY Any value 50+ char random, from env var
ALLOWED_HOSTS ['*'] Your actual domain(s) and IP
DATABASES SQLite PostgreSQL
CONN_MAX_AGE 0 60 (saves reconnect overhead)
CSRF_COOKIE_SECURE False True after SSL is live

Settings verified against Django 6.0 deployment checklist, 2026-06-21

Now run migrations and collect static files. Skipping collectstatic is the most common beginner mistake — Gunicorn doesn’t serve static files, only Django’s dev server does:

python manage.py migrate
python manage.py createsuperuser
python manage.py collectstatic --noinput

Run the deployment check to catch any remaining issues:

python manage.py check --deploy

Fix everything this command flags before moving on.

Step 7: Configure Gunicorn with systemd

Test Gunicorn manually first to confirm your app loads correctly:

cd /var/www/django_app
source venv/bin/activate
gunicorn --bind 0.0.0.0:8000 myproject.wsgi

If that works (you can hit port 8000 in a browser), kill it and set up the proper systemd service. The modern approach uses a Unix socket — not a TCP port — for Nginx to Gunicorn communication. Unix sockets skip the TCP/IP stack entirely, which measurably reduces latency on high-throughput apps.

Create the socket unit at /etc/systemd/system/gunicorn.socket:

[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/run/gunicorn.sock

[Install]
WantedBy=sockets.target

Create the service unit at /etc/systemd/system/gunicorn.service:

[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

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

[Install]
WantedBy=multi-user.target

Replace myproject.wsgi with your actual Django project’s module name. The EnvironmentFile= directive is what loads your .env — this is how SECRET_KEY and DB_PASSWORD reach your app without being in your codebase.

Enable and start the socket:

sudo systemctl daemon-reload
sudo systemctl start gunicorn.socket
sudo systemctl enable gunicorn.socket
sudo systemctl status gunicorn.socket

Verify the socket file was created:

file /run/gunicorn.sock
# Expected: /run/gunicorn.sock: socket
Terminal output showing gunicorn.socket status active and /run/gunicorn.sock file confirmed
Terminal output showing gunicorn.socket status active and /run/gunicorn.sock file confirmed

Step 8: Configure Nginx

Create a new Nginx server block at /etc/nginx/sites-available/myproject:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    location = /favicon.ico { access_log off; log_not_found off; }

    location /static/ {
        root /var/www/django_app;
    }

    location /media/ {
        root /var/www/django_app;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}

💡 Tip: Want a head start on the config? Our free Nginx Config Generator builds a working server block you can adapt to your setup.

Enable the site, test the config, and restart Nginx:

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

nginx -t catches syntax errors before they take down your server. Never skip it.

Step 9: UFW Firewall Rules

Open the right ports for Nginx. Nginx Full opens both port 80 (HTTP) and port 443 (HTTPS):

sudo ufw delete allow 8000      # Remove dev port if you opened it earlier
sudo ufw allow 'Nginx Full'
sudo ufw status

At this point, http://yourdomain.com should serve your Django app through Nginx and Gunicorn. If you get a 502 Bad Gateway, Gunicorn isn’t running — check with sudo systemctl status gunicorn and sudo journalctl -u gunicorn --no-pager -n 50.

Step 10: SSL with Let’s Encrypt (Certbot)

You shouldn’t run a production Django app on HTTP. Certbot automates the full SSL setup:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot handles everything: obtains the certificate from Let’s Encrypt, modifies your Nginx config to enable port 443, and sets up automatic HTTP → HTTPS redirect. The python3-certbot-nginx plugin rewrites your Nginx config directly, so you don’t have to.

Verify auto-renewal is working:

sudo systemctl status certbot.timer
sudo certbot renew --dry-run

After SSL is confirmed, the CSRF_COOKIE_SECURE = True and SESSION_COOKIE_SECURE = True settings you added earlier become active. Restart Gunicorn to pick up the changes:

sudo systemctl restart gunicorn
Browser showing Django site with valid HTTPS padlock and domain configured on Vultr VPS
Browser showing Django site with valid HTTPS padlock and domain configured on Vultr VPS

Production Checklist Before You Go Live

Run through this before you point real traffic at the server:

  • DEBUG = False in production settings
  • SECRET_KEY loaded from .env, not hardcoded
  • ALLOWED_HOSTS contains only your actual domain(s)
  • PostgreSQL running — no SQLite
  • python manage.py collectstatic has been run
  • python manage.py check --deploy passes with no errors
  • SSL certificate valid (https:// works in browser)
  • CSRF_COOKIE_SECURE = True and SESSION_COOKIE_SECURE = True enabled
  • UFW firewall active with only SSH and Nginx Full open
  • gunicorn.socket enabled to start on boot

PostgreSQL: Same VPS or Vultr Managed Database?

Running PostgreSQL on the same VPS works fine for low-traffic apps and side projects. The setup you just completed is the right choice if you’re launching something new, testing in production, or running a personal project.

For apps where data loss is not acceptable — SaaS products, paid services, anything with real users — Vultr’s Managed Database for PostgreSQL is worth the additional cost. Starting around $15–20/month, it gives you automatic backups with point-in-time recovery, high availability with automatic failover, and zero maintenance (Vultr handles patches and PostgreSQL upgrades). The network latency vs. localhost is minimal when you pick the same datacenter region.

vultr

The practical rule: if losing your database would mean losing money or users’ data, the managed DB is cheap insurance. If you’re running your own blog or a personal portfolio, same-VPS Postgres is fine.

Troubleshooting Common Issues

502 Bad Gateway from Nginx: Almost always means Gunicorn isn’t running. Check sudo systemctl status gunicorn and sudo journalctl -u gunicorn -n 50 --no-pager for the error.

Static files returning 404: You forgot to run python manage.py collectstatic. Django’s dev server serves static files automatically; Gunicorn doesn’t. Run collectstatic and restart Gunicorn.

DisallowedHost error (400 Bad Request): ALLOWED_HOSTS is misconfigured. With DEBUG = False, Django rejects any request whose Host header doesn’t match. Add your domain and server IP to the list.

Socket permission errors (Nginx can’t connect to Gunicorn): Nginx runs as www-data. Check that the service file has Group=www-data in the [Service] block, and verify with:

sudo -u www-data stat /run/gunicorn.sock

Static files returning 403 Forbidden: Nginx’s www-data user can’t read the files. This is the exact reason we put the app under /var/www/ rather than a home directory — home directories block traversal for non-owner users by default.

Viewing Logs

With DEBUG = False, Python exceptions no longer appear in the browser (that’s intentional). Check logs here:

# Gunicorn errors and access logs
sudo journalctl -u gunicorn -f

# Nginx access log
sudo tail -f /var/log/nginx/access.log

# Nginx error log
sudo tail -f /var/log/nginx/error.log

For production error tracking, add ADMINS to your Django settings for email alerts, or integrate Sentry for full stack traces.

FAQ

Can I use Docker instead of Gunicorn + Nginx on Vultr?

Yes — and it’s a legitimate alternative for teams that want environment consistency across dev and prod. If you’re considering that path, we covered the full setup in our Deploy Docker on Vultr VPS guide. The tradeoff: Docker adds deployment complexity and a small overhead; Gunicorn + Nginx is leaner and easier to debug on a single VPS.

How many Gunicorn workers do I need?

Use (2 × CPU cores) + 1. A 1 vCPU Vultr instance gets 3 workers. Each worker handles one request at a time, so 3 workers means 3 concurrent requests before queuing starts. For most low-to-medium traffic Django apps, 3 workers on a $12/month VPS is plenty.

Do I need to point a domain at my Vultr VPS before starting?

No — you can complete the entire setup with just the server IP and configure DNS later. Use your IP in ALLOWED_HOSTS during initial testing, then swap in your domain once DNS propagates. You’ll need a domain pointed at your IP before running Certbot for SSL.

Is Vultr good for Django hosting?

For developers who want control over their environment and aren’t paying for managed infrastructure they don’t need, Vultr’s High Performance NVMe plans are a solid choice. The VPS hosting basics covers when a VPS makes more sense than shared hosting — relevant reading if you’re making the switch from a shared host.

What’s the minimum Vultr plan for Django?

$6/month (1 vCPU, 1 GB RAM) can run Django + Gunicorn + Nginx + PostgreSQL on a light-traffic site. Realistically, the $12/month plan with 2 GB RAM is the minimum for anything you’re expecting real users on. RAM is the constraint, not CPU — each Gunicorn worker consumes ~50 MB, and PostgreSQL adds another 50–100 MB before your app code runs.

Final Verdict

The setup in this guide — Ubuntu 24.04, Python 3.12, Gunicorn via systemd socket, Nginx reverse proxy, PostgreSQL, Certbot SSL — is the 2026 production standard for single-server Django deployments. It’s not the simplest path (that would be a managed platform like Railway or Render), but it gives you full control over your stack, predictable pricing, and no vendor lock-in.

For most independent developers and small teams, Vultr’s $12/month High Performance plan hits the right balance of price and headroom. Start there, monitor your actual RAM usage under load, and scale vertically when you need to.

vultr

If you’re already comfortable running this setup and want to explore infrastructure that scales beyond a single VPS, the next logical step is containerizing your app — our Docker on Vultr guide covers that transition.

New to Vultr? Sign up through our link and get $300 free credit — valid for 30 days, limited-time offer. Claim your credit →

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top