How to Deploy a Python Flask or Django App on a VPS with Gunicorn and Nginx
Python powers some of the world’s most successful web applications — Instagram, Pinterest, Spotify, and countless others all run on Django or Flask backends. Deploying these apps to a VPS gives you complete control over your runtime environment, database, and infrastructure — without the constraints or costs of managed platform services.
This guide covers deploying both Flask and Django applications on Ubuntu VPS using Gunicorn as the WSGI server and Nginx as the reverse proxy — the production-standard stack for Python web apps.
The Production Python Stack
| Component | Role |
|---|---|
| Python app | Your Flask or Django application code |
| Gunicorn | WSGI server — translates HTTP requests for Python |
| Nginx | Reverse proxy — handles SSL, static files, load balancing |
| Systemd | Keeps Gunicorn running and restarts on crash/reboot |
| Virtualenv | Isolates Python dependencies per project |
Step 1: Update System and Install Python
sudo apt update && sudo apt upgrade -y
sudo apt install python3 python3-pip python3-venv nginx -y
Step 2: Create Your Application Directory
sudo mkdir -p /var/www/myapp
sudo chown -R $USER:$USER /var/www/myapp
cd /var/www/myapp
# Create a virtual environment
python3 -m venv venv
source venv/bin/activate
Part A: Flask Deployment
Install Flask and Gunicorn
pip install flask gunicorn
Create a sample Flask app
nano app.py
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/')
def index():
return jsonify({"message": "Flask running on VPS.DO!", "status": "ok"})
@app.route('/health')
def health():
return jsonify({"status": "healthy"})
if __name__ == '__main__':
app.run()
Create a WSGI entry point
nano wsgi.py
from app import app
if __name__ == "__main__":
app.run()
Test Gunicorn
gunicorn --bind 0.0.0.0:8000 wsgi:app
# Visit http://YOUR_VPS_IP:8000 to confirm it works
# Press Ctrl+C to stop
Part B: Django Deployment
Install Django and Gunicorn
pip install django gunicorn psycopg2-binary python-dotenv
Create or deploy your Django project
# New project
django-admin startproject myproject .
# Or clone existing project
git clone https://github.com/you/myproject.git .
pip install -r requirements.txt
Configure Django for production
nano myproject/settings.py
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com', 'YOUR_VPS_IP']
STATIC_ROOT = '/var/www/myapp/static/'
STATIC_URL = '/static/'
# Database (PostgreSQL recommended for production)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'myapp_db',
'USER': 'myapp_user',
'PASSWORD': 'yourpassword',
'HOST': 'localhost',
}
}
Run migrations and collect static files
python manage.py migrate
python manage.py collectstatic --noinput
python manage.py createsuperuser
Test Django with Gunicorn
gunicorn --bind 0.0.0.0:8000 myproject.wsgi:application
Step 3: Create a Systemd Service for Gunicorn
This keeps Gunicorn running 24/7 and restarts it after crashes or reboots:
sudo nano /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn daemon for Python app
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
Environment="PATH=/var/www/myapp/venv/bin"
# For Flask:
ExecStart=/var/www/myapp/venv/bin/gunicorn --workers 3 --bind unix:/var/www/myapp/gunicorn.sock wsgi:app
# For Django (use this instead):
# ExecStart=/var/www/myapp/venv/bin/gunicorn --workers 3 --bind unix:/var/www/myapp/gunicorn.sock myproject.wsgi:application
Restart=always
[Install]
WantedBy=multi-user.target
sudo chown -R www-data:www-data /var/www/myapp
sudo systemctl daemon-reload
sudo systemctl enable gunicorn
sudo systemctl start gunicorn
sudo systemctl status gunicorn
How many Gunicorn workers?
Rule of thumb: (2 × CPU cores) + 1
- 2 vCPU → 5 workers
- 4 vCPU → 9 workers
Step 4: Configure Nginx as Reverse Proxy
sudo nano /etc/nginx/sites-available/myapp
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Serve Django/Flask static files directly (much faster than Python)
location /static/ {
root /var/www/myapp;
expires 30d;
add_header Cache-Control "public, no-transform";
}
location /media/ {
root /var/www/myapp;
}
# Proxy all other requests to Gunicorn
location / {
proxy_pass http://unix:/var/www/myapp/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_read_timeout 300s;
}
access_log /var/log/nginx/myapp.access.log;
error_log /var/log/nginx/myapp.error.log;
}
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Step 5: Add SSL
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Step 6: Environment Variables and Secrets
Never store secrets in your code. Use a .env file:
nano /var/www/myapp/.env
SECRET_KEY=your-django-secret-key
DATABASE_URL=postgresql://myapp_user:yourpassword@localhost/myapp_db
DEBUG=False
ALLOWED_HOSTS=yourdomain.com
chmod 600 /var/www/myapp/.env
Load in your app with python-dotenv:
from dotenv import load_dotenv
load_dotenv()
import os
SECRET_KEY = os.environ.get('SECRET_KEY')
Step 7: Deployment Updates (Zero Downtime)
nano ~/deploy.sh
#!/bin/bash
set -e
cd /var/www/myapp
source venv/bin/activate
git pull origin main
pip install -r requirements.txt
python manage.py migrate --noinput
python manage.py collectstatic --noinput
sudo systemctl reload gunicorn
echo "Deployed successfully ✅"
chmod +x ~/deploy.sh
Gunicorn’s reload (not restart) gracefully replaces workers one at a time — zero downtime deployments from a single script.
Final Thoughts
The Gunicorn + Nginx + Systemd stack is battle-tested Python production infrastructure. It handles thousands of concurrent requests, serves static files efficiently, survives reboots, and deploys updates gracefully — all running on a VPS.DO KVM VPS starting at $20/month.
Related articles: