Deploying Python Web Apps on a VPS: A Working Setup
When building a web application with Python frameworks like Django, Flask, or FastAPI, development is usually straightforward. You write your code, spin up a local development server using python manage.py runserver or uvicorn main:app --reload, and instantly see your changes in the browser.
However, moving that application from your local machine or a managed platform-as-a-service (PaaS) to a production Virtual Private Server (VPS) requires a completely different operational mindset. Development servers are single-threaded, highly insecure, and designed to crash on errors rather than recover gracefully.
To run a Python web app reliably in production, you need a robust, battle-tested stack: Gunicorn or Uvicorn to handle Python application workers, Systemd to manage process lifecycles, and Nginx to act as a secure, high-performance reverse proxy.
This comprehensive guide breaks down a standard, production-ready setup for deploying Python web applications on an Ubuntu VPS.
Step 1: Provisioning the VPS and Setting Up the Environment
Before touching any application code, your server must be prepared with the correct system dependencies, a secure non-root user, and an isolated Python virtual environment.
1. Update Packages and Install Python Tools
Log into your VPS via SSH and update your system package repositories, then install Python, pip, and Nginx:
Bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3 python3-pip python3-venv nginx git curl ufw
2. Clone Your Repository and Create a Virtual Environment
Navigate to your web directory (typically /var/www/), clone your application repository, and create a dedicated virtual environment:
Bash
sudo mkdir -p /var/www/myapp
sudo chown -R $USER:$USER /var/www/myapp
cd /var/www/myapp
git clone https://github.com/yourusername/your-repo.git .
# Create and activate a virtual environment
python3 -m venv venv
source venv/bin/activate
3. Install Application Dependencies
With your virtual environment active, install your project dependencies using your requirements.txt or pyproject.toml file:
Bash
pip install --upgrade pip
pip install -r requirements.txt
Step 2: Choosing and Configuring Your Application Server
Python web frameworks do not natively speak HTTP in a scalable, production-ready manner. Instead, they rely on interface standards:
WSGI (Web Server Gateway Interface): Used for synchronous frameworks like Django and Flask. Gunicorn is the industry standard WSGI container.
ASGI (Asynchronous Server Gateway Interface): Used for asynchronous frameworks like FastAPI or Django Channels. Uvicorn is the primary ASGI server, which can also run as worker processes inside Gunicorn.
Testing Gunicorn Manually
For a standard Flask or Django app, test running Gunicorn directly from your terminal to verify everything is wired correctly:
Bash
# For Flask / WSGI
gunicorn --bind 127.0.0.1:8000 myapp.wsgi:application
# For FastAPI / ASGI (using Uvicorn workers)
gunicorn -k uvicorn.workers.UvicornWorker main:app --bind 127.0.0.1:8000 --workers 4
If your application responds correctly on port 8000, you are ready to automate the process so it runs continuously in the background.
Step 3: Managing Processes with Systemd
Never leave your Python application running via a manual terminal command. If your SSH session disconnects or the server reboots, your app will go offline.
Systemd is the native Linux service manager that keeps your application alive across crashes and system reboots, and restarts it automatically when deployments occur.
Creating a Systemd Service File
Create a new service file for your application:
Bash
sudo nano /etc/systemd/system/myapp.service
Add the following configuration, ensuring you replace paths, usernames, and command arguments to match your specific environment:
Ini, TOML
[Unit]
Description=Gunicorn instance to serve myapp
After=network.target
[Service]
User=ubuntu
Group=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/var/www/myapp/venv/bin/gunicorn --workers 3 --bind unix:/var/www/myapp/myapp.sock myapp.wsgi:application
Restart=on-failure
[Install]
WantedBy=multi-user.target
Why Use a Unix Socket?
Instead of binding Gunicorn to a local TCP port (like 127.0.0.1:8000), configuring Gunicorn to communicate over a Unix socket (myapp.sock) offers a slight performance boost and enhanced security because network traffic stays entirely within the local file system between Nginx and Gunicorn.
Enable and Start the Service
Reload Systemd to register the new service, start it, and configure it to launch automatically on boot:
Bash
sudo systemctl daemon-reload
sudo systemctl start myapp
sudo systemctl enable myapp
sudo systemctl status myapp
Step 4: Configuring Nginx as a Reverse Proxy
Never expose your Gunicorn or Uvicorn socket directly to the public internet. Nginx must sit in front of your application to manage client connections, handle SSL encryption, compress HTTP payloads, and serve static assets efficiently.
Creating an Nginx Server Block
Create a new configuration file for your domain:
Bash
sudo nano /etc/nginx/sites-available/myapp
Paste the following production server block configuration:
Nginx
server {
listen 80;
server_name yourdomain.co.ke www.yourdomain.co.ke;
# Serve static files directly without hitting Python
location /static/ {
alias /var/www/myapp/staticfiles/;
expires 30d;
access_log off;
}
# Proxy dynamic requests to the Gunicorn Unix socket
location / {
include proxy_params;
proxy_pass http://unix:/var/www/myapp/myapp.sock;
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;
}
}
Enable the Site and Test Nginx
Link the configuration file to the sites-enabled directory, check for syntax errors, and restart Nginx:
Bash
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
Step 5: Securing Your App with Free SSL (Certbot)
Running a modern web app without HTTPS is a security risk that also damages your search engine rankings. Secure your VPS domain easily using Certbot:
Bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.co.ke -d www.yourdomain.co.ke
Certbot will automatically update your Nginx configuration to enforce HTTPS redirection and configure automated certificate renewals.
Step 6: Handling Static Files and Database Migrations
When pushing updates to your production environment, follow a strict deployment sequence to avoid downtime:
Pull Code Changes:
git pull origin mainUpdate Dependencies:
source venv/bin/activate && pip install -r requirements.txtRun Database Migrations: Execute your framework-specific migration commands (e.g.,
python manage.py migratefor Django or Alembic upgrades for FastAPI).Collect Static Assets: Run asset compilation commands (e.g.,
python manage.py collectstatic) so Nginx can serve updated CSS, JavaScript, and images.Reload the Application: Gracefully reload Gunicorn without dropping active connections:
Bash
sudo systemctl restart myapp
Frequently Asked Questions (FAQs)
1. What is the difference between Gunicorn and Uvicorn?
Gunicorn is a traditional WSGI server built for synchronous Python frameworks like Flask and Django. Uvicorn is an ASGI server designed for asynchronous, high-concurrency frameworks like FastAPI. For asynchronous apps, you typically run Gunicorn with Uvicorn worker classes to combine multi-process stability with async performance.
2. Why should I use a Unix socket instead of an IP port?
A Unix socket communicates via a local file path rather than network loopback TCP ports. This eliminates network overhead stack processing between Nginx and Gunicorn, yielding faster internal communication and closing off potential external port exposure risks.
3. How do I view application logs if my Python app crashes?
Since Systemd manages your app process, standard output and error logs are captured by the system journal. You can view live error logs and trace exceptions by running sudo journalctl -u myapp -f.
4. Why are my static files (CSS/JS) returning 404 errors?
This usually happens because Nginx cannot locate the static directory specified in the alias directive. Ensure you have run your framework's static collection command (such as collectstatic) and that the www-data system user has read permissions across the directory tree.
5. How do I handle environment variables securely on a VPS?
Never commit your .env secrets file to public Git repositories. Instead, create a secure .env file directly inside your application root on the VPS (/var/www/myapp/.env) and configure your Python application loader (like python-dotenv or Django settings) to read values securely from the operating system environment.
Written by Mercy Chepkemoi
SteboHost engineering and operations team. We write practical guides on hosting, domains, email and security for African businesses — based on what we see in the field, not marketing copy.
Ready to get started?
Browse our hosting plans or contact our team for help choosing the right one.