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 ForestsNet Cloud 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).
ℹ️ Tip: there's no ready-made recipe specifically for FastAPI in VMmanager 6. The closest one is Django (Django + uWSGI + Nginx, ID 1) — but it's built for Django, not for the FastAPI/Gunicorn/Uvicorn combination. See Ready recipes for VMmanager 6 for the full catalog of one-click installs. Below is the manual setup, which works for any FastAPI application.
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-workerℹ️ Tip: the uvicorn-worker package isn't optional filler. The uvicorn.workers.UvicornWorker class that used to ship inside uvicorn itself is marked deprecated in Uvicorn's official docs and has been moved into a separate package, uvicorn-worker (class uvicorn_worker.UvicornWorker). That's the one to pass to --worker-class — the old import path may still work on your installed Uvicorn version, but it's no longer something to rely on.
Create 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
EOFℹ️ Tip: Type=notify is Gunicorn's officially recommended setting (supported since version 20.0: Gunicorn notifies systemd it's ready via sd_notify). If the unit hangs in activating and then times out after you start it, switch Type=notify to Type=simple — that's a known edge case on some versions.
Enable 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"}.
ℹ️ Tip (AlmaLinux/Rocky Linux): by default SELinux doesn't let nginx open outgoing network connections — the reverse proxy will respond with 502 Bad Gateway instead. Check the httpd_can_network_connect boolean and enable it if needed:
getsebool httpd_can_network_connect
sudo setsebool -P httpd_can_network_connect on⚠️ Important: for port 80 to be reachable from outside, you need to open it in the firewall — see step 5 below, or the Setting up a firewall (ufw) article.
Step 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.
⚠️ Important: before enabling the firewall, make sure the SSH port (usually 22/tcp) is definitely allowed — otherwise you can lock yourself out of the server.
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-allℹ️ Tip: on a fresh image SSH is usually already allowed in firewalld by default, but adding the rule again is harmless — it won't error. If you add HTTPS later (for example, Let's Encrypt/certbot), remember to open 443/tcp too (ufw allow 443/tcp or --add-service=https). See the Setting up a firewall (ufw) article for more on managing the firewall.
✅ Done: the app is reachable from outside at http://example.com/ (or by IP), runs under systemd — surviving reboots and individual worker crashes — and sits behind nginx.
What'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).