Python programming code on laptop

Deploy Python Flask/Django on VPS with uWSGI and Nginx

June 2026 · 8 min read

Deploying a Python web application on a VPS offers multiple approaches, but the most popular and efficient combination is using uWSGI as a WSGI server paired with Nginx as a reverse proxy. This guide will walk you through every step from setup to production-ready deployment for both Flask and Django applications.

Why Use uWSGI + Nginx?

Python web frameworks like Flask and Django come with built-in development servers, but these should never be used in production. They lack security features and cannot handle concurrent requests efficiently. A proper production stack requires the following architecture:

Step 1: Prepare VPS and Python Environment

Begin by updating the system and installing required packages (assuming Ubuntu 22.04 LTS)

# Update system first
sudo apt update && sudo apt upgrade -y

# Install Python, pip, virtualenv, and build tools
sudo apt install -y python3 python3-pip python3-venv python3-dev \
    build-essential libssl-dev libffi-dev nginx

# Check versions
python3 --version
nginx -v

Next, create a separate user account to run the application (a security best practice)

sudo useradd -m -s /bin/bash appuser
sudo su - appuser

Step 2: Create and Deploy Python App

For Flask

# Create project directory
mkdir -p /home/appuser/myapp
cd /home/appuser/myapp

# Create virtual environment
python3 -m venv venv
source venv/bin/activate

# Install Flask and uWSGI
pip install flask uwsgi

# Create main application file
cat > app.py << 'EOF'
from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return '

Hello from Flask on VPS!

' @app.route('/health') def health(): return {'status': 'ok'} if __name__ == '__main__': app.run() EOF

For Django

# Install Django and uWSGI
pip install django uwsgi

# Create new Django project
django-admin startproject myproject .

# Adjust ALLOWED_HOSTS in settings.py
# ALLOWED_HOSTS = ['your-domain.com', 'your-server-ip']

# Run migrations
python manage.py migrate
Note: For Django, replace app:app with myproject.wsgi:application in the uWSGI configuration step below

Step 3: Configure uWSGI

Create a configuration file for uWSGI

cat > /home/appuser/myapp/uwsgi.ini << 'EOF'
[uwsgi]
# For Flask
module = app:app

# For Django use this line instead:
# module = myproject.wsgi:application

master = true
processes = 4
threads = 2

# Socket file for communication with Nginx
socket = /tmp/myapp.sock
chmod-socket = 660
vacuum = true

# Logging
logto = /var/log/uwsgi/myapp.log

# Improve stability
die-on-term = true
EOF

Create the log directory and test uWSGI

sudo mkdir -p /var/log/uwsgi
sudo chown appuser:www-data /var/log/uwsgi

# Test if uWSGI works
cd /home/appuser/myapp
source venv/bin/activate
uwsgi --ini uwsgi.ini
Warning: If uWSGI fails with "unable to load configuration", verify that paths in uwsgi.ini are correct and the virtual environment is properly activated

Step 4: Configure systemd Service

Create a systemd service so uWSGI starts automatically when the server restarts

sudo nano /etc/systemd/system/myapp.service

Add the following content to the file

[Unit]
Description=uWSGI instance for myapp
After=network.target

[Service]
User=appuser
Group=www-data
WorkingDirectory=/home/appuser/myapp
Environment="PATH=/home/appuser/myapp/venv/bin"
ExecStart=/home/appuser/myapp/venv/bin/uwsgi --ini uwsgi.ini

Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp

# Check status
sudo systemctl status myapp

Step 5: Configure Nginx

Create an Nginx virtual host configuration

sudo nano /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name your-domain.com www.your-domain.com;

    # Logging
    access_log /var/log/nginx/myapp_access.log;
    error_log  /var/log/nginx/myapp_error.log;

    # Static files (for Django)
    location /static/ {
        alias /home/appuser/myapp/staticfiles/;
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }

    # Media files (for Django)
    location /media/ {
        alias /home/appuser/myapp/media/;
        expires 7d;
    }

    # Proxy all requests to uWSGI
    location / {
        include uwsgi_params;
        uwsgi_pass unix:/tmp/myapp.sock;
        uwsgi_read_timeout 60;
        uwsgi_connect_timeout 60;
    }

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
}
# Enable site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/

# Check syntax
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

Step 6: Add SSL with Let's Encrypt

Using HTTPS is essential in production. Use Certbot to obtain a free SSL certificate from Let's Encrypt

# Install Certbot
sudo apt install -y certbot python3-certbot-nginx

# Request certificate (replace with your actual domain)
sudo certbot --nginx -d your-domain.com -d www.your-domain.com

# Test auto-renewal
sudo certbot renew --dry-run
Note: Certbot automatically modifies your Nginx config, adding HTTPS redirect and SSL certificate paths

Step 7: Configure Static Files (Django)

For Django, you must run collectstatic before deploying every time

cd /home/appuser/myapp
source venv/bin/activate

# Configure in settings.py
# STATIC_ROOT = '/home/appuser/myapp/staticfiles/'

# Collect static files
python manage.py collectstatic --noinput

# Restart service
sudo systemctl restart myapp

Monitoring and Debugging

Common commands for checking and troubleshooting issues

# View uWSGI logs in real-time
sudo journalctl -u myapp -f

# View Nginx error logs
sudo tail -f /var/log/nginx/myapp_error.log
sudo tail -f /var/log/nginx/myapp_access.log

# Check socket file
ls -la /tmp/myapp.sock

# Restart service after code changes
sudo systemctl restart myapp

# View all service statuses
sudo systemctl status myapp nginx

Deploying New Code (Workflow)

  1. Upload or pull new code to the server
  2. Activate virtual environment: source venv/bin/activate
  3. Install new dependencies: pip install -r requirements.txt
  4. For Django run migrations: python manage.py migrate
  5. For Django collect static files: python manage.py collectstatic --noinput
  6. Restart uWSGI service: sudo systemctl restart myapp
Warning: Restarting the service will cause brief downtime. For zero-downtime deployment, use sudo systemctl reload myapp or consider implementing blue-green deployment

Security Checklist Before Production


Summary

Deploying Python Flask or Django on a VPS using uWSGI and Nginx creates a stable, secure, and well-scalable stack for production environments. The main steps to remember are:

  1. Prepare environment — Python venv and install dependencies
  2. Configure uWSGI — Create uwsgi.ini and systemd service
  3. Configure Nginx — Virtual host that proxies to uWSGI socket
  4. Add SSL — Use Let's Encrypt via Certbot
  5. Test and monitor — Check logs and service status

Once you've completed all steps, your Python app is ready to handle real-world traffic reliably. For future scaling, you can increase the number of processes in your uWSGI config or use Nginx as a load balancer across multiple servers.

Verify Python App is Working Correctly

# Check uWSGI service status
sudo systemctl status myapp

# Verify socket file was created
ls -la /tmp/myapp.sock

# Test app response through Nginx
curl -I http://localhost/
# Should return HTTP/1.1 200 OK

# Watch logs in real-time for errors
sudo journalctl -u myapp -f --no-pager | head -20

# Check Nginx error log
sudo tail -20 /var/log/nginx/myapp_error.log
Tip: If systemctl status myapp shows active (running) and the socket file exists but you still get a 502 error, check socket permissions — Nginx must have read access to the socket

Common Issues & Troubleshooting

Error: 502 Bad Gateway from Nginx

Nginx cannot reach the uWSGI socket, usually due to permission issues or incorrect paths

# Verify socket exists and Nginx can read it
ls -la /tmp/myapp.sock
# Should show something like srwxrwx--- with www-data permissions

# Fix permissions in uwsgi.ini
# chmod-socket = 664
# chown-socket = appuser:www-data

# Check Nginx error log for actual error
sudo tail -20 /var/log/nginx/myapp_error.log

Error: "ModuleNotFoundError" or "ImportError" when uWSGI starts

uWSGI is not using Python from the virtual environment — PATH is wrong in systemd service

# Verify virtualenv path is correct in myapp.service
cat /etc/systemd/system/myapp.service | grep "Environment\|ExecStart"

# Should have:
# Environment="PATH=/home/appuser/myapp/venv/bin"
# ExecStart=/home/appuser/myapp/venv/bin/uwsgi --ini uwsgi.ini

# If changed, reload daemon first
sudo systemctl daemon-reload
sudo systemctl restart myapp

Django: "DisallowedHost" error

ALLOWED_HOSTS in settings.py doesn't include your domain or server IP

# Edit settings.py
ALLOWED_HOSTS = ['your-domain.com', 'www.your-domain.com', 'YOUR_SERVER_IP']

# Or use environment variables
import os
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')

# Restart uWSGI after changes
sudo systemctl restart myapp

Django: "collectstatic" error — STATIC_ROOT not set

Must define STATIC_ROOT in settings.py before running collectstatic

# Add to settings.py
STATIC_ROOT = '/home/appuser/myapp/staticfiles/'
STATICFILES_DIRS = [BASE_DIR / 'static']

# Run collectstatic
cd /home/appuser/myapp
source venv/bin/activate
python manage.py collectstatic --noinput

# Verify Nginx location /static/ points to correct path
grep -A2 "location /static" /etc/nginx/sites-available/myapp

uWSGI Crash loop — "unable to load configuration" or respawn too fast

Check error logs first; often caused by syntax errors in Python code or uwsgi.ini

# Run uWSGI manually to see errors directly
cd /home/appuser/myapp
source venv/bin/activate
uwsgi --ini uwsgi.ini

# Verify Python app can be imported
python -c "from app import app; print('OK')"  # Flask
python -c "import myproject.wsgi; print('OK')"  # Django

Frequently Asked Questions

Should I use uWSGI or Gunicorn?

Both work well. Gunicorn is simpler with less configuration, better for beginners. uWSGI offers more features like async workers, Emperor mode for managing multiple apps, and sometimes better performance. For typical Flask/Django apps, Gunicorn is sufficient — simply use gunicorn app:app -w 4 --bind unix:/tmp/myapp.sock instead of uWSGI.

Why must I use a virtual environment?

Virtual environments isolate Python packages per project, preventing version conflicts. For example, App A might need Django 4.2 while App B needs Django 5.0 — without venv, both can't coexist. Additionally, it prevents polluting system Python that Ubuntu/Debian relies on for system tools.

How do I achieve zero-downtime deployment?

uWSGI supports graceful reload via the SIGHUP signal — new workers start before old ones stop accepting requests

# Graceful reload — no downtime
sudo systemctl reload myapp
# or send signal directly
sudo kill -HUP $(cat /tmp/myapp.pid)