Deploy Python Flask/Django on VPS with uWSGI and Nginx
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:
- Nginx — Receives HTTP/HTTPS requests from the internet, handles static files, performs SSL termination, and manages load balancing
- uWSGI — Converts requests from Nginx into a format that Python apps understand via the WSGI protocol, manages worker processes and threading
- Flask/Django — Processes business logic and returns responses
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
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
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
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)
- Upload or pull new code to the server
- Activate virtual environment:
source venv/bin/activate - Install new dependencies:
pip install -r requirements.txt - For Django run migrations:
python manage.py migrate - For Django collect static files:
python manage.py collectstatic --noinput - Restart uWSGI service:
sudo systemctl restart myapp
sudo systemctl reload myapp or consider implementing blue-green deployment
Security Checklist Before Production
- Set
DEBUG = Falsein Django settings - Configure
SECRET_KEYwith a unique random value; never hardcode it - Use environment variables for sensitive configuration (try
python-dotenv) - Set
ALLOWED_HOSTSto specific values, never use* - Enable HTTPS and redirect all HTTP traffic
- Configure firewall to only open ports 80, 443, and 22
- Update dependencies regularly to fix security vulnerabilities
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:
- Prepare environment — Python venv and install dependencies
- Configure uWSGI — Create
uwsgi.iniand systemd service - Configure Nginx — Virtual host that proxies to uWSGI socket
- Add SSL — Use Let's Encrypt via Certbot
- 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
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 socketCommon 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)