Python's built-in development servers are not meant for real traffic. The standard way to deploy a Python app is Gunicorn as the application server, systemd to keep it running and Nginx in front for HTTPS and static files. The same steps work for Flask, Django and FastAPI (with Uvicorn workers).
Before you start
- An Ubuntu or Debian VPS with sudo access (commands for AlmaLinux are similar with
dnf). - A domain pointing to the server's IP.
- Your app in a Git repository with a
requirements.txt.
Install packages and create a user
apt update && apt install python3-venv python3-pip nginx gitadduser --system --group --home /srv/myapp myappRunning the app as its own unprivileged user limits damage if it is ever compromised.
Set up the app and virtualenv
sudo -u myapp git clone https://github.com/you/myapp.git /srv/myapp/appsudo -u myapp python3 -m venv /srv/myapp/venvsudo -u myapp /srv/myapp/venv/bin/pip install -r /srv/myapp/app/requirements.txt gunicornFor Django, also run migrate and collectstatic, set DEBUG = False and add your domain to ALLOWED_HOSTS.
Test Gunicorn by hand (Flask app object app in app.py; for Django use myproject.wsgi:application):
cd /srv/myapp/app && sudo -u myapp ../venv/bin/gunicorn --bind 127.0.0.1:8000 app:appStop it with Ctrl+C once it responds to curl 127.0.0.1:8000.
Create a systemd service
Create /etc/systemd/system/myapp.service:
[Unit]
Description=myapp Gunicorn
After=network.target
[Service]
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp/app
EnvironmentFile=/srv/myapp/app/.env
ExecStart=/srv/myapp/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 --access-logfile - app:app
Restart=on-failure
[Install]
WantedBy=multi-user.targetA common starting point is (2 × CPU cores) + 1 workers. For FastAPI add -k uvicorn.workers.UvicornWorker. Start it:
systemctl daemon-reload && systemctl enable --now myappConfigure Nginx as a reverse proxy
Create /etc/nginx/sites-available/myapp:
server {
listen 80;
server_name example.com www.example.com;
location /static/ {
alias /srv/myapp/app/static/;
}
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 it and reload:
ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ && nginx -t && systemctl reload nginxAdd free SSL
apt install certbot python3-certbot-nginx && certbot --nginx -d example.com -d www.example.comCertbot edits the Nginx config for HTTPS and sets up automatic renewal.
Deploy updates
cd /srv/myapp/app && sudo -u myapp git pull && sudo -u myapp ../venv/bin/pip install -r requirements.txt && systemctl restart myappCheck that it works
systemctl status myappshows active (running).journalctl -u myapp -fstreams Gunicorn logs.- Open
https://example.com; static files should load from Nginx.
Troubleshooting
- 502 Bad Gateway: Gunicorn is not running or listens on a different address. Check the service log and the
proxy_passport. - ModuleNotFoundError: the service is not using the virtualenv, or the
WorkingDirectoryis wrong. - WORKER TIMEOUT: a request runs longer than 30 seconds. Fix the slow code or raise
--timeout. - Static files 404: the
aliaspath must end with a slash and be readable by Nginx.
Python deployment FAQ
Gunicorn or uWSGI?
Gunicorn is simpler to configure and is the common default today. Both are production-ready.
Can I run Python apps on cPanel hosting?
Where the server offers it, cPanel → Software → Setup Python App (CloudLinux Python Selector) runs WSGI apps without root access.
Do I need Nginx in front of Gunicorn?
Yes in production: it handles TLS, slow clients and static files far better than Gunicorn alone.
