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.
What you need
| Spec | Minimum | Recommended |
|---|---|---|
| CPU | 1 vCPU | 2 vCPU |
| RAM | 1 GB | 2 GB |
| Storage | 20 GB SSD | 40 GB SSD |
| Python | 3.11 | 3.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.targetEnable 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 = False— Never run with debug enabled in productionSECRET_KEY— Loaded from environment variable, not hardcodedALLOWED_HOSTS— Explicitly list your domain(s) and server IPSECURE_SSL_REDIRECT = True— Redirect all HTTP requests to HTTPSSESSION_COOKIE_SECURE = True— Only send session cookies over HTTPSCSRF_COOKIE_SECURE = True— Only send CSRF cookies over HTTPSSECURE_HSTS_SECONDS = 31536000— Tell browsers to always use HTTPS (1 year)SECURE_HSTS_INCLUDE_SUBDOMAINS = True— Apply HSTS to all subdomainsX_FRAME_OPTIONS = 'DENY'— Prevent your site from being embedded in iframesSECURE_CONTENT_TYPE_NOSNIFF = True— Prevent 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
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.
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).
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;
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.
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.
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 DashboardFrequently 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.