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.

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.
| 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 sourcelibpq-dev— PostgreSQL client libraries; psycopg2 won’t build without thispostgresql,postgresql-contrib— database server running locallynginx— 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-binaryis fine for development and most production deployments. If you’re compiling for a specific architecture or have specific performance requirements, installpsycopg2(without-binary) and let it compile against the systemlibpq-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

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

Production Checklist Before You Go Live
Run through this before you point real traffic at the server:
DEBUG = Falsein production settingsSECRET_KEYloaded from.env, not hardcodedALLOWED_HOSTScontains only your actual domain(s)- PostgreSQL running — no SQLite
python manage.py collectstatichas been runpython manage.py check --deploypasses with no errors- SSL certificate valid (
https://works in browser) CSRF_COOKIE_SECURE = TrueandSESSION_COOKIE_SECURE = Trueenabled- UFW firewall active with only SSH and Nginx Full open
gunicorn.socketenabled 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.
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.
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.

