Python and FastAPI: production deployment
By the end of this guide your FastAPI app will run under Gunicorn with Uvicorn worker processes, start automatically on reboot via systemd, and serve external requests through nginx — ready for real traffic, not just uvicorn main:app --reload on your laptop.
What you'll need
- An mHost VPS on one of these OS templates: Ubuntu 22.04/24.04, Debian 11/12, AlmaLinux/Rocky Linux 8/9.
- SSH access as root or a user with sudo.
- A domain pointing at the server's IP — for nginx's
server_name. If you don't have one yet, the server's IP address works too. - A FastAPI app of your own (or use the minimal example in this guide, just to verify the whole chain from venv to nginx).
Step 1. Set up a user, venv, and dependencies
The app will run not as root but as a dedicated system user with no login shell — this limits the damage if a vulnerability is ever found in the app itself.
Install Python and the virtual environment module:
Ubuntu / Debian
sudo apt update
sudo apt install python3-venv python3-pipAlmaLinux / Rocky Linux
sudo dnf install python3 python3-pipCreate a dedicated system user for the app. The --create-home flag also creates its home directory, /opt/myapp, where both the code and the venv will live:
sudo useradd --system --create-home --home-dir /opt/myapp --shell /usr/sbin/nologin myappSwitch to that user (it has no login shell, so start bash as an explicit command) and create the virtual environment:
sudo -u myapp -H bash
cd /opt/myapp
python3 -m venv venv
source venv/bin/activateInstall FastAPI, Uvicorn, and Gunicorn:
pip install --upgrade pip
pip install fastapi "uvicorn[standard]" gunicorn uvicorn-workerCreate a minimal main.py — if you already have a working app, skip this step and copy your own code into /opt/myapp instead:
cat > /opt/myapp/main.py << 'EOF'
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"status": "ok"}
EOFCheck that the app actually starts — directly through Uvicorn, without Gunicorn yet:
uvicorn main:app --host 127.0.0.1 --port 8000 &
sleep 1
curl http://127.0.0.1:8000/
kill %1This should return {"status":"ok"}.
Step 2. Run Gunicorn with Uvicorn workers
Gunicorn acts as the process manager: it spins up several parallel workers, restarts any that crash, and can upgrade the app with no downtime — but Gunicorn only understands the ASGI protocol that FastAPI speaks through the uvicorn_worker.UvicornWorker worker class.
The worker count is set with the -w/--workers flag. Gunicorn's official recommendation is 2–4 workers per CPU core (run nproc to see how many cores you have). For a small 1-2 core VPS, 3-4 workers is usually enough.
Test the combination right now, while you're still in the myapp user's session with the venv active:
gunicorn main:app \
--workers 3 \
--worker-class uvicorn_worker.UvicornWorker \
--bind 127.0.0.1:8000 &
sleep 1
curl http://127.0.0.1:8000/
kill %1
exitThe exit at the end takes you back to your sudo user's session — everything from here on runs under that account.
Step 3. Set up a systemd unit
Create the unit at /etc/systemd/system/myapp.service:
sudo tee /etc/systemd/system/myapp.service > /dev/null << 'EOF'
[Unit]
Description=Gunicorn (Uvicorn workers) for myapp
After=network.target
[Service]
Type=notify
NotifyAccess=main
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/myapp/venv/bin/gunicorn main:app \
--workers 3 \
--worker-class uvicorn_worker.UvicornWorker \
--bind 127.0.0.1:8000 \
--access-logfile - \
--error-logfile - \
--timeout 60
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
Restart=on-failure
RestartSec=5
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOFEnable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myappCheck the logs (including the request logs Gunicorn writes) through journald:
sudo journalctl -u myapp -fStep 4. Configure nginx as a reverse proxy
Install nginx:
Ubuntu / Debian
sudo apt install nginxAlmaLinux / Rocky Linux
sudo dnf install nginx
sudo systemctl enable --now nginxCreate the site config. The file path differs by distro — pick the right CONF= line:
# Ubuntu / Debian:
CONF=/etc/nginx/sites-available/myapp
# AlmaLinux / Rocky Linux:
CONF=/etc/nginx/conf.d/myapp.conf
sudo tee "$CONF" > /dev/null << 'EOF'
upstream myapp {
server 127.0.0.1:8000 fail_timeout=0;
}
server {
listen 80;
server_name example.com;
client_max_body_size 20m;
location / {
proxy_pass http://myapp;
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;
}
}
EOF
# Ubuntu / Debian only — enable the site with a symlink:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myappReplace example.com with your own domain (or the server's IP address if you don't have a domain yet). fail_timeout=0 on the upstream block means "don't mark the backend as unavailable after a single failed attempt, keep retrying it" — which makes sense when there's only one Gunicorn process behind the upstream and nothing else to fail over to.
Check the configuration and reload nginx:
sudo nginx -t
sudo systemctl reload nginxVerify from outside:
curl http://example.com/This should return {"status":"ok"}.
getsebool httpd_can_network_connect
sudo setsebool -P httpd_can_network_connect onStep 5. Open the port in the firewall
Gunicorn itself only listens on 127.0.0.1 and is never reachable from outside — the only port you actually need to open is nginx's.
Ubuntu / Debian (ufw)
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw enable
sudo ufw status verboseAlmaLinux / Rocky Linux (firewalld)
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --reload
sudo firewall-cmd --list-allWhat's next
- The logical next step is to get a TLS certificate and turn on HTTPS (for example, via Let's Encrypt/certbot) on top of the nginx setup you now have — that's a separate topic outside the scope of this guide.
- Updating the app is now simple: update the code in
/opt/myapp(for example,git pullas themyappuser) and restart the service withsudo systemctl restart myapp. - For the full catalog of one-click installs in VMmanager 6, see Ready recipes for VMmanager 6; firewall management is covered in more detail in Setting up a firewall (ufw).