Host a Python web app with Gunicorn
Python web frameworks such as Flask and Django are not meant to face the internet directly. Gunicorn runs your app as a production-ready server, and nginx sits in front of it to handle visitors. This guide sets up a Flask example in a virtual environment, runs it as a service, and connects nginx to it.
Before you begin
- A VPS running Ubuntu 24.04 LTS, with SSH access as a sudo user (this guide uses
alex). - nginx installed (see Host a static website for the basics). The example uses the
example.comdomain. - Basic Python knowledge. Your own app can replace the example file.
Step 1: Install Python tools
Ubuntu 24.04 blocks pip from installing packages system-wide, to protect the operating system. The fix is a virtual environment: a private copy of Python for one project. The python3-venv package provides the tool that creates them.
sudo apt update
sudo apt install -y python3 python3-venv
Python 3.12.3
python3 --version
Step 2: Create the project and a virtual environment
Create a folder for the project, then create a virtual environment inside it. The source command activates the environment, so commands like pip use the project's copy of Python.
mkdir -p ~/apps/pyapp
cd ~/apps/pyapp
python3 -m venv venv
source venv/bin/activate
pip install flask gunicorn
Your prompt now starts with (venv), which means the environment is active. Packages you install here stay inside the venv folder.
Step 3: Write a small app
Create app.py. The file defines a variable named app, which Gunicorn will load.
nano app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello from Gunicorn and Flask\n"
Test it with Gunicorn. The argument app:app means "the app variable inside the app.py module". Open a second SSH session and run curl, or check the output after you stop Gunicorn with Ctrl+C:
gunicorn --bind 127.0.0.1:8000 app:app
curl http://127.0.0.1:8000
Hello from Gunicorn and Flask
Step 4: Run Gunicorn as a service
Create a systemd service so the app starts on boot and restarts after a crash. Note the full path to Gunicorn inside the virtual environment, which is what makes the service use the right packages.
sudo nano /etc/systemd/system/pyapp.service
[Unit]
Description=Gunicorn for pyapp
After=network.target
[Service]
User=alex
WorkingDirectory=/home/alex/apps/pyapp
ExecStart=/home/alex/apps/pyapp/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 app:app
Restart=on-failure
[Install]
WantedBy=multi-user.target
The --workers 3 option runs three worker processes, so one slow request does not block everyone else. A common starting point is two workers per CPU core plus one. Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now pyapp
sudo systemctl status pyapp --no-pager
Active: active (running)
Step 5: Put nginx in front
nginx listens on the public port 80 and forwards requests to Gunicorn on 127.0.0.1:8000. This arrangement is called a reverse proxy.
sudo nano /etc/nginx/sites-available/pyapp
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
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, remove the default welcome site, and open the web ports in the firewall if they are not already open:
sudo ln -s /etc/nginx/sites-available/pyapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
sudo ufw allow 'Nginx Full'
Step 6: Test the whole path
curl -i http://example.com
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/html; charset=utf-8
Hello from Gunicorn and Flask
Troubleshooting
- "externally-managed-environment" when running pip. You are installing outside a virtual environment. Activate
venvwithsource venv/bin/activate, then run pip again. - 502 Bad Gateway from nginx. Gunicorn is not running or not on port 8000. Check with
sudo ss -tlnp | grep 8000andsudo journalctl -u pyapp -n 50 --no-pager. - ModuleNotFoundError in the logs. The package was installed outside the venv. Activate it and run
pip installagain, then restart the service. - Changes to app.py do not show. Gunicorn loads code at startup. Run
sudo systemctl restart pyapp.
Next steps
Add HTTPS so traffic is encrypted, and tune the number of workers as your traffic grows. The same reverse-proxy pattern works for Node.js apps, as shown in Host a Node.js app with systemd.
Something not working? Chat or email our support team and we will help you get unstuck.
Contact support