FastAPI is a Python framework for building HTTP APIs on top of the ASGI standard. In production it runs under Uvicorn, an ASGI server, with Nginx in front to terminate TLS and forward requests. In this tutorial you will deploy a FastAPI application on Ubuntu 24.04 in a Python virtual environment, run it as a systemd service under its own user, publish it through Nginx and secure it with a Let's Encrypt certificate.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 1 GB of RAM.
  • A non-root user with sudo privileges.
  • A domain name with a DNS A record pointing to your server's public IP. This guide uses api.your_domain as the placeholder; replace it with your own hostname.
  • Ports 80 and 443 reachable from the Internet.

Step 1 - Installing Python and Nginx

Ubuntu 24.04 ships Python 3.12, which current FastAPI releases support. Install the venv module so you can create an isolated environment, plus Nginx:

sudo apt update
sudo apt install -y python3-venv python3-pip nginx

Check the Python version:

python3 --version
Python 3.12.3

Step 2 - Creating an application user and virtual environment

Running the API as its own unprivileged user limits the damage if the application is ever compromised. Create a system user called fastapi without a login shell:

sudo useradd --system --home-dir /opt/myapi --shell /usr/sbin/nologin fastapi

Create the application directory and give it to that user:

sudo mkdir -p /opt/myapi/app
sudo chown -R fastapi:fastapi /opt/myapi

Create a virtual environment inside it and install FastAPI and Uvicorn. The standard extra installs uvloop and httptools, which make Uvicorn noticeably faster:

sudo -u fastapi python3 -m venv /opt/myapi/venv
sudo -u fastapi /opt/myapi/venv/bin/pip install fastapi "uvicorn[standard]"

Confirm both packages are installed:

sudo -u fastapi /opt/myapi/venv/bin/pip list | grep -iE '^(fastapi|uvicorn) '
fastapi           0.118.0
uvicorn           0.37.0

Your version numbers will be newer. When you deploy your own project, install its dependencies from a pinned requirements.txt instead, with sudo -u fastapi /opt/myapi/venv/bin/pip install -r /opt/myapi/app/requirements.txt.

Step 3 - Creating the application

If you already have a FastAPI project, copy it to /opt/myapi/app (for example with git clone or rsync) and make sure it is owned by fastapi. Otherwise, create a small example application to follow along:

sudo -u fastapi nano /opt/myapi/app/main.py
import os

from fastapi import FastAPI

# Hide the interactive docs in production unless explicitly enabled
show_docs = os.getenv("ENABLE_DOCS", "false").lower() == "true"

app = FastAPI(
    title="My API",
    docs_url="/docs" if show_docs else None,
    redoc_url=None,
    openapi_url="/openapi.json" if show_docs else None,
)


@app.get("/")
async def root():
    return {"message": "Hello from FastAPI"}


@app.get("/health")
async def health():
    return {"status": "ok"}

Test that the application starts, binding only to the loopback interface:

cd /opt/myapi
sudo -u fastapi /opt/myapi/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000
INFO:     Started server process [2415]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

In a second SSH session, send a request:

curl http://127.0.0.1:8000/health
{"status":"ok"}

Stop the test server with CTRL+C.

Step 4 - Storing configuration in an environment file

Secrets such as database URLs and API keys should not live in the code. Keep them in a file that only root can read; systemd reads it as root and passes the values to the process:

sudo mkdir -p /etc/myapi
sudo nano /etc/myapi/myapi.env
ENABLE_DOCS=false
DATABASE_URL=postgresql://myapi:your_strong_password@localhost/myapi
SECRET_KEY=replace_with_a_long_random_value

Replace the example values with your own. You can generate a random secret with openssl rand -hex 32. Then restrict the file:

sudo chmod 600 /etc/myapi/myapi.env

Your application reads these values with os.getenv() or a settings library such as pydantic-settings.

Step 5 - Running FastAPI as a systemd service

A systemd unit starts the API at boot, restarts it if it crashes and sends its output to the journal. Create the unit file:

sudo nano /etc/systemd/system/myapi.service
[Unit]
Description=My FastAPI application
After=network.target

[Service]
Type=exec
User=fastapi
Group=fastapi
WorkingDirectory=/opt/myapi
EnvironmentFile=/etc/myapi/myapi.env
ExecStart=/opt/myapi/venv/bin/uvicorn app.main:app \
    --host 127.0.0.1 \
    --port 8000 \
    --workers 2 \
    --proxy-headers \
    --forwarded-allow-ips 127.0.0.1
Restart=on-failure
RestartSec=5

# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true

[Install]
WantedBy=multi-user.target

A few details:

  • --workers 2 starts two worker processes managed by Uvicorn. A good starting point is one worker per CPU core; check yours with nproc.
  • --proxy-headers and --forwarded-allow-ips 127.0.0.1 make Uvicorn trust the X-Forwarded-For and X-Forwarded-Proto headers that Nginx sends, so your application sees the real client IP and the https scheme.
  • ProtectSystem=strict mounts the whole filesystem read-only for the service. If your application needs to write files, add the directory with ReadWritePaths=/var/lib/myapi.

Load the unit and start it:

sudo systemctl daemon-reload
sudo systemctl enable --now myapi.service

Check its status:

sudo systemctl status myapi.service
● myapi.service - My FastAPI application
     Loaded: loaded (/etc/systemd/system/myapi.service; enabled; preset: enabled)
     Active: active (running) since Thu 2026-09-25 10:32:04 UTC; 5s ago
   Main PID: 3102 (uvicorn)

Test it again locally:

curl http://127.0.0.1:8000/
{"message":"Hello from FastAPI"}

If the service does not start, read its logs with sudo journalctl -u myapi.service -n 50.

Step 6 - Configuring Nginx as a reverse proxy

Nginx receives public traffic on port 80 and 443 and forwards it to Uvicorn on 127.0.0.1:8000. Create a server block for your domain:

sudo nano /etc/nginx/sites-available/myapi
server {
    listen 80;
    listen [::]:80;
    server_name api.your_domain;

    client_max_body_size 10m;

    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;
        proxy_read_timeout 60s;
    }
}

client_max_body_size sets the largest request body Nginx accepts; raise it if your API receives file uploads. Enable the site, test the configuration and reload Nginx:

sudo ln -s /etc/nginx/sites-available/myapi /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Allow SSH and web traffic through UFW and enable the firewall if it is not active yet:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

From your own computer, the API now answers over plain HTTP:

curl http://api.your_domain/health
{"status":"ok"}

Step 7 - Enabling HTTPS with Let's Encrypt

Certbot obtains a free certificate from Let's Encrypt and updates the Nginx configuration for you:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d api.your_domain

Certbot asks for an email address for expiry notices and for your agreement to the terms of service. When it finishes, it has added the certificate to your server block and a redirect from HTTP to HTTPS. Verify the result:

curl -I https://api.your_domain/health
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Content-Type: application/json

Certificates are renewed automatically by the certbot.timer systemd timer. Check that renewal works with a dry run:

sudo certbot renew --dry-run

Step 8 - Deploying updates

To release a new version, update the code, install any new dependencies and restart the service. With a Git checkout this looks like:

sudo -u fastapi git -C /opt/myapi/app pull
sudo -u fastapi /opt/myapi/venv/bin/pip install -r /opt/myapi/app/requirements.txt
sudo systemctl restart myapi.service

The restart takes a second or two. Follow the logs afterwards to confirm the new version started cleanly:

sudo journalctl -u myapi.service -f

Troubleshooting

Nginx returns 502 Bad Gateway. Nginx cannot reach Uvicorn. Check that the service is running with sudo systemctl status myapi.service, that something listens on port 8000 with sudo ss -tlnp | grep 8000, and read /var/log/nginx/error.log for the exact error.

The service exits with ModuleNotFoundError. Uvicorn cannot import your application. The app.main:app argument is relative to WorkingDirectory, so /opt/myapi/app/main.py must exist and define a variable called app. Dependencies must be installed in /opt/myapi/venv, not system-wide.

Environment variables are empty in the application. Confirm the path in EnvironmentFile= is correct and that each line uses KEY=value without export. After editing the unit or the file, run sudo systemctl daemon-reload and sudo systemctl restart myapi.service.

Redirects point to http:// instead of https://. Uvicorn is not trusting the proxy headers. Make sure the unit includes --proxy-headers --forwarded-allow-ips 127.0.0.1 and the Nginx block sets X-Forwarded-Proto.

Requests time out on slow endpoints. Raise proxy_read_timeout in the Nginx block. For work that takes more than a few seconds, move it to a background task queue instead of keeping the HTTP request open.

Conclusion

Your FastAPI application now runs under Uvicorn as a hardened systemd service, restarts automatically, and is served over HTTPS by Nginx with automatic certificate renewal. The same layout works for any ASGI application.

As next steps, you can add a PostgreSQL database for persistent data, forward the journal to a central log server, and put a monitoring check on the /health endpoint so you are alerted when the API stops responding.