How to Deploy a Python Flask or Django App on a VPS with Gunicorn and Nginx

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:

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!