A reliable FastAPI deployment on Ubuntu 24.04 uses four clear boundaries: a Python virtual environment for dependencies, Uvicorn for the ASGI application server, systemd for process supervision, and Nginx for the public HTTP/HTTPS edge. In this tutorial, Uvicorn listens only on 127.0.0.1:8000; port 8000 is never opened to the internet.
Raff Technologies is used as the Ubuntu VM platform in the saved test workflow. The original deployment was tested on a Raff Ubuntu 24.04 VM with 1 vCPU and 2 GB RAM. For this refresh, the current package and deployment guidance was re-verified rather than claiming a new full machine test.
As of September 7, 2026, the latest PyPI releases verified for this guide are FastAPI 0.141.1 and Uvicorn 0.52.4. Ubuntu 24.04 remains on the Python 3.12 branch; the Noble Updates package is currently based on Python 3.12.3. For a real application, use the dependency versions you have tested and locked instead of upgrading production dependencies blindly during server setup.
Prerequisites:
- A Raff Linux VM running Ubuntu 24.04
- SSH access with a non-root sudo user
- A domain pointed to the VM if you want public HTTPS
- Ports
80/tcpand443/tcpavailable for Nginx - A recovery path before enabling or changing UFW
- No public firewall rule for port
8000
Step 1 — Install Python, Nginx, and firewall tools
Update Ubuntu and install the required packages:
sudo apt update sudo apt upgrade -y sudo apt install -y python3 python3-venv python3-pip nginx ufw curl
Check the installed Python and Ubuntu package revision:
python3 --version apt-cache policy python3.12 | sed -n '1,8p'
Ubuntu 24.04 uses Python 3.12 as its default Python branch. The exact Ubuntu package revision can increase through security and maintenance updates.
Inspect UFW before changing it:
sudo ufw status verbose
Confirm the SSH port the server is actually configured to use:
sudo sshd -T | awk '/^port / {print $2}'
If UFW is already active, preserve the working SSH rule and allow Nginx:
sudo ufw allow 'Nginx Full' sudo ufw status numbered
If UFW is inactive, allow the real SSH path first. For standard SSH with the OpenSSH profile:
sudo ufw allow OpenSSH
For a custom SSH port, allow that port instead, for example:
sudo ufw allow 2222/tcp
Then allow Nginx:
sudo ufw allow 'Nginx Full'
Keep the current SSH session open and successfully open a second SSH session before enabling an inactive firewall:
sudo ufw enable sudo ufw status numbered
Do not use ufw --force enable to bypass this safety check.
Verify: Python should report 3.12.x, Nginx should be active, the actual SSH path should remain reachable, and UFW should allow only the public services you intend to expose.
Step 2 — Create a dedicated FastAPI service user and application directory
Create a system account that cannot log in interactively:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin fastapi
Create the application directory:
sudo mkdir -p /srv/fastapi-app sudo chown -R fastapi:fastapi /srv/fastapi-app sudo chmod 750 /srv/fastapi-app
A dedicated service account limits what the application process can access compared with running it as root or as a general administrator account.
Verify:
getent passwd fastapi stat -c '%U:%G %a %n' /srv/fastapi-app
The account should use a non-login shell and /srv/fastapi-app should be owned by fastapi:fastapi.
Step 3 — Create a virtual environment and install FastAPI with Uvicorn
Create an isolated Python environment:
sudo -u fastapi python3 -m venv /srv/fastapi-app/.venv
For this reproducible demo, install the versions verified during this refresh:
sudo -u fastapi /srv/fastapi-app/.venv/bin/python -m pip install --upgrade pip sudo -u fastapi /srv/fastapi-app/.venv/bin/python -m pip install \ 'fastapi[standard]==0.141.1' \ 'uvicorn[standard]==0.52.4'
A real application should normally install from its tested requirements or lockfile instead. Keeping packages inside the virtual environment avoids modifying Ubuntu's externally managed system Python.
For Python isolation details, see Install pip on Ubuntu 24.04 and Use Python venv.
Verify:
sudo -u fastapi /srv/fastapi-app/.venv/bin/python -c \ 'import fastapi, uvicorn; print("FastAPI", fastapi.__version__); print("Uvicorn", uvicorn.__version__)'
The demo environment should report FastAPI 0.141.1 and Uvicorn 0.52.4.
Step 4 — Create a minimal FastAPI application and health endpoint
Create /srv/fastapi-app/main.py:
sudo tee /srv/fastapi-app/main.py > /dev/null <<'PY' from fastapi import FastAPI app = FastAPI(title="Example API") @app.get("/") async def root(): return {"status": "ok"} @app.get("/health") async def health(): return {"status": "healthy"} PY
Set ownership and permissions:
sudo chown fastapi:fastapi /srv/fastapi-app/main.py sudo chmod 640 /srv/fastapi-app/main.py
Verify:
sudo -u fastapi /srv/fastapi-app/.venv/bin/python -m py_compile \ /srv/fastapi-app/main.py
The command should exit without a syntax error.
Step 5 — Test Uvicorn on localhost only
Start the application manually before creating a service:
cd /srv/fastapi-app sudo -u fastapi ./.venv/bin/uvicorn main:app \ --host 127.0.0.1 \ --port 8000
In a second SSH session, inspect the listener:
sudo ss -lntp | grep ':8000'
Test the health endpoint:
curl -sS http://127.0.0.1:8000/health
Expected response:
{"status":"healthy"}
Stop the manual process with Ctrl+C after the test.
Verify: Uvicorn should respond locally on 127.0.0.1:8000, and ss should not show 0.0.0.0:8000.
Step 6 — Create a protected environment file
Create a configuration directory outside the source tree:
sudo install -d -m 750 -o root -g fastapi /etc/fastapi
Create an environment file:
sudo tee /etc/fastapi/fastapi.env > /dev/null <<'EOF' APP_ENV=production APP_SECRET=replace-with-a-real-secret EOF
Restrict it:
sudo chown root:fastapi /etc/fastapi/fastapi.env sudo chmod 640 /etc/fastapi/fastapi.env
Do not commit secrets to Git or place them in Nginx configuration.
Verify:
stat -c '%U:%G %a %n' /etc/fastapi/fastapi.env
The expected ownership is root:fastapi with mode 640.
Step 7 — Create the FastAPI systemd service
Create /etc/systemd/system/fastapi.service:
sudo tee /etc/systemd/system/fastapi.service > /dev/null <<'EOF' [Unit] Description=FastAPI application After=network.target [Service] Type=simple User=fastapi Group=fastapi WorkingDirectory=/srv/fastapi-app EnvironmentFile=/etc/fastapi/fastapi.env ExecStart=/srv/fastapi-app/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000 --proxy-headers --forwarded-allow-ips=127.0.0.1 Restart=on-failure RestartSec=5 NoNewPrivileges=true PrivateTmp=true ProtectHome=true ProtectSystem=strict ProtectKernelTunables=true ProtectControlGroups=true RestrictSUIDSGID=true [Install] WantedBy=multi-user.target EOF
The proxy options are deliberately restrictive. FastAPI documents forwarded headers such as X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host, but the application server should trust them only from a known proxy. Here Nginx and Uvicorn run on the same VM, so the trusted proxy address is 127.0.0.1.
If your real application writes uploads, generated files, SQLite data, or other local state, grant only the required writable paths instead of weakening the entire systemd sandbox.
Reload systemd and start the service:
sudo systemctl daemon-reload sudo systemctl enable --now fastapi
Verify:
sudo systemctl is-active fastapi sudo systemctl is-enabled fastapi curl -sS http://127.0.0.1:8000/health
The service should be active, enabled, and healthy on localhost.
Step 8 — Inspect FastAPI service logs
Read recent logs:
sudo journalctl -u fastapi -n 50 --no-pager
Follow logs while troubleshooting:
sudo journalctl -u fastapi -f
Inspect the exact unit and process state:
sudo systemctl cat fastapi sudo systemctl show fastapi -p MainPID -p ExecMainStatus -p NRestarts
Verify: the journal should show a successful Uvicorn startup without a repeated crash/restart loop.
Step 9 — Configure Nginx as the FastAPI reverse proxy
Create the Nginx site:
sudo tee /etc/nginx/sites-available/fastapi > /dev/null <<'EOF' server { listen 80; listen [::]:80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; 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; proxy_set_header X-Forwarded-Host $host; proxy_connect_timeout 10s; proxy_read_timeout 60s; } } EOF
X-Forwarded-Host is included explicitly because current FastAPI proxy guidance identifies host, protocol, and client-address forwarding as part of the original request information that may need to be preserved behind a proxy.
Do not add WebSocket Upgrade headers unless your application actually serves WebSocket endpoints.
Enable the site:
sudo ln -sf /etc/nginx/sites-available/fastapi \ /etc/nginx/sites-enabled/fastapi sudo rm -f /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx
For Nginx fundamentals, see Install Nginx on Ubuntu 24.04.
Verify:
curl -sS http://127.0.0.1/health -H 'Host: your-domain.com' sudo nginx -t
Nginx should return the FastAPI health response and the configuration test should pass.
Step 10 — Decide whether FastAPI docs should be public
FastAPI exposes Swagger UI at /docs and the OpenAPI schema at /openapi.json by default. Public developer APIs may intentionally expose them, while private APIs may restrict them.
If documentation should remain public, the main Nginx proxy is enough.
If it should be private, prefer application authentication or an intentional application configuration. A simple proxy-level deny can also hide the endpoints from public traffic:
location = /docs { return 404; } location = /openapi.json { return 404; }
Hiding documentation is not a replacement for API authentication and authorization.
Verify: request /docs and /openapi.json from the public side and confirm the result matches your intended exposure policy.
Step 11 — Enable HTTPS with Certbot
Make sure DNS points to this VM and port 80 is publicly reachable, then install Certbot:
sudo apt install -y certbot python3-certbot-nginx
Request the certificate and redirect HTTP to HTTPS:
sudo certbot --nginx --redirect -d your-domain.com
Inspect the certificate and test renewal:
sudo certbot certificates sudo certbot renew --dry-run
For ACME troubleshooting, use Secure Nginx with Let's Encrypt on Ubuntu 24.04.
Verify:
curl -I http://your-domain.com/health curl -sS https://your-domain.com/health
HTTP should redirect to HTTPS and the HTTPS health endpoint should return {"status":"healthy"}.
Step 12 — Add Uvicorn workers only after measuring the application
FastAPI currently supports multiple worker processes directly through the FastAPI or Uvicorn CLI. Gunicorn is not required simply to use more CPU cores.
For example, a two-worker Uvicorn command is:
/srv/fastapi-app/.venv/bin/uvicorn main:app \ --host 127.0.0.1 \ --port 8000 \ --workers 2 \ --proxy-headers \ --forwarded-allow-ips=127.0.0.1
Do not use a universal workers-per-CPU formula. Each worker is a separate process with its own application memory, database connections, and startup work. Measure CPU, memory, latency, throughput, and connection-pool pressure before changing the worker count.
Older guides often use uvicorn.workers.UvicornWorker with Gunicorn. Uvicorn currently marks the built-in uvicorn.workers module as deprecated. If your organization chooses Gunicorn for process-management features, verify the current supported worker package and compatibility instead of copying that deprecated import path.
Verify: after any worker-count change, confirm the expected process count, service health, and public endpoint:
ps -ef | grep '[u]vicorn' sudo systemctl is-active fastapi curl -sS https://your-domain.com/health
Step 13 — Verify the FastAPI deployment end to end and after reboot
Check the complete stack:
printf 'Python:\n' python3 --version printf '\nFastAPI and Uvicorn:\n' sudo -u fastapi /srv/fastapi-app/.venv/bin/python -c \ 'import fastapi, uvicorn; print(fastapi.__version__, uvicorn.__version__)' printf '\nFastAPI service:\n' sudo systemctl is-active fastapi sudo systemctl is-enabled fastapi printf '\nLocal health:\n' curl -sS http://127.0.0.1:8000/health printf '\nListener:\n' sudo ss -lntp | grep ':8000' printf '\nNginx:\n' sudo nginx -t printf '\nHTTPS:\n' curl -sS https://your-domain.com/health printf '\nFirewall:\n' sudo ufw status numbered
Confirm that:
- Python is on the intended Ubuntu 3.12 branch;
- the virtual environment contains the dependency versions you tested;
fastapi.serviceis active and enabled;- Uvicorn listens only on
127.0.0.1:8000; - Nginx passes its configuration test;
- HTTPS serves the expected health response;
- port 8000 has no public firewall rule; and
- forwarded headers are trusted only from the intended proxy path.
Then reboot during a maintenance window:
sudo reboot
After reconnecting:
sudo systemctl is-active fastapi nginx curl -sS http://127.0.0.1:8000/health curl -sS https://your-domain.com/health
End-to-end verification is complete when FastAPI and Nginx return automatically after reboot, the API is healthy over HTTPS, and Uvicorn remains private on loopback.
