# Deploy a FastAPI app with Gunicorn, Uvicorn workers and systemd

> Run a FastAPI app in a virtualenv under Gunicorn with Uvicorn workers, keep it up with a systemd service, and put nginx in front as a reverse proxy.

Source: https://vpsnine.com/tutorials/deploy-fastapi-gunicorn-nginx · Updated: 2026-10-21

Difficulty: intermediate · Time: about 30 minutes · Tested on: Ubuntu 24.04 LTS

Prerequisites:

- A server running Ubuntu 24.04
- Root or a user with sudo
- A domain name pointed at the server (for HTTPS)
- Your FastAPI app's code

To deploy a FastAPI app, install it in a Python virtual environment, run it with Gunicorn using Uvicorn workers (from the `uvicorn-worker` package), keep Gunicorn running with a systemd service, and put nginx in front to take the traffic on ports 80 and 443. This tutorial does each step on Ubuntu 24.04 with a small example app, and shows how to reload new code without dropping requests.

Gunicorn is the process manager: it starts several worker processes, restarts any that crash or hang, and reloads them gracefully. Each worker is a Uvicorn server running your async app. nginx handles HTTPS, slow clients and request size limits, and passes requests to Gunicorn on `127.0.0.1`.

| What | In this tutorial |
| --- | --- |
| The app's system user | `api` |
| The app's directory | `/srv/api`, with the code in `/srv/api/app` |
| Where Gunicorn listens | `127.0.0.1:8000` |
| The domain | `api.example.com` |

## Install Python and nginx

Ubuntu 24.04 ships Python 3.12. Install the venv module and nginx:

```bash
sudo apt update
sudo apt install -y python3-venv python3-pip nginx
python3 --version
```

Install packages into a virtual environment, never with `sudo pip`: Ubuntu blocks system-wide pip installs (`externally-managed-environment`) so they can't break the system's own Python tools.

## Create a user and directory for the app

A system user with no login shell runs the app, so a bug in it can't touch the rest of the server:

```bash
sudo useradd --system --create-home --home-dir /srv/api --shell /usr/sbin/nologin api
```

Put your code in `/srv/api/app` (with `git clone`, `rsync` or `scp`) and give it to the `api` user:

```bash
sudo chown -R api:api /srv/api
```

If you want a small app to follow along with, create `/srv/api/app/main.py`:

```python
from fastapi import FastAPI

app = FastAPI()


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


@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}
```

Add an empty `/srv/api/app/__init__.py` next to it, so `app` is a regular Python package.

## Create the virtual environment

Create the venv as the `api` user and install FastAPI, Gunicorn and the Uvicorn worker:

```bash
sudo -u api python3 -m venv /srv/api/.venv
sudo -u api /srv/api/.venv/bin/pip install fastapi gunicorn uvicorn-worker
```

For your own app, install from its requirements file instead (`pip install -r /srv/api/app/requirements.txt`) and make sure `gunicorn` and `uvicorn-worker` are in it.

Why `uvicorn-worker`: the worker class that used to ship inside Uvicorn, `uvicorn.workers.UvicornWorker`, is deprecated. Importing it in Uvicorn 0.54 prints:

```text
DeprecationWarning: The `uvicorn.workers` module is deprecated. Please use `uvicorn-worker` package instead.
```

The replacement is the same worker, maintained separately, under the name `uvicorn_worker.UvicornWorker`.

Check it worked:

```bash
sudo -u api /srv/api/.venv/bin/pip list | grep -iE "fastapi|gunicorn|uvicorn"
```

On 21 October 2026 that listed fastapi 0.159.1, gunicorn 26.2.0, uvicorn 0.54.0 and uvicorn-worker 0.4.0.

## Configure Gunicorn

Settings in a file are easier to read and change than a long command line. Create `/srv/api/gunicorn.conf.py`:

```python
bind = "127.0.0.1:8000"
workers = 2
worker_class = "uvicorn_worker.UvicornWorker"
forwarded_allow_ips = "127.0.0.1"
accesslog = "-"
errorlog = "-"
timeout = 60
```

- `bind` keeps Gunicorn on localhost: only nginx can reach it, so nobody bypasses the proxy.
- `workers`: each worker is a separate process with its own copy of the app. Start with one or two per CPU core and watch memory; async apps usually need fewer than sync ones. Two suits a server with 1 or 2 cores.
- `forwarded_allow_ips` tells the workers to trust the `X-Forwarded-*` headers from nginx, so `request.client.host` is the visitor's address and `request.url.scheme` says `https`.
- `accesslog` and `errorlog` set to `-` send logs to standard output, which systemd's journal collects.

Run it by hand once, from the app's parent directory:

```bash
cd /srv/api
sudo -u api /srv/api/.venv/bin/gunicorn app.main:app -c /srv/api/gunicorn.conf.py
```

`app.main:app` means "the object `app` in the module `app/main.py`". In a second SSH session, check it worked:

```bash
curl http://127.0.0.1:8000/items/5?q=test
```

```json
{"item_id":5,"q":"test"}
```

Stop Gunicorn with Ctrl+C.

## Run it as a systemd service

systemd starts Gunicorn at boot, restarts it if it dies and sends it the reload signal. Create `/etc/systemd/system/api.service`:

```ini
[Unit]
Description=FastAPI app (gunicorn)
After=network.target

[Service]
Type=notify
NotifyAccess=main
User=api
Group=api
WorkingDirectory=/srv/api
ExecStart=/srv/api/.venv/bin/gunicorn app.main:app -c /srv/api/gunicorn.conf.py
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=30
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full

[Install]
WantedBy=multi-user.target
```

- `Type=notify` with `NotifyAccess=main`: Gunicorn tells systemd when it is ready, so `systemctl start` waits until it really is. This follows Gunicorn's own [systemd example](https://docs.gunicorn.org/en/stable/deploy.html#systemd).
- `ExecReload` sends `HUP`, which makes Gunicorn start new workers with the new code and stop the old ones gracefully.
- `KillMode=mixed` lets Gunicorn shut its workers down itself when the service stops.
- `NoNewPrivileges`, `PrivateTmp` and `ProtectSystem=full` take away things a web app never needs.

If the app needs secrets (a database URL, an API key), put them in `/etc/api.env`, readable by root only (`sudo chmod 600 /etc/api.env`), and add `EnvironmentFile=/etc/api.env` under `[Service]`. systemd reads the file before it drops to the `api` user.

Start the service and enable it at boot:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now api
```

Check it worked:

```bash
systemctl status api
curl http://127.0.0.1:8000/
```

The status is `active (running)` with a master and two worker processes, and curl prints `{"status":"ok"}`. Follow the logs with `journalctl -u api -f`.

## Put nginx in front

Create `/etc/nginx/sites-available/api`:

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name api.example.com;

    client_max_body_size 10m;

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

`client_max_body_size` caps uploads (nginx's default is 1 MB); `proxy_read_timeout` matches Gunicorn's `timeout`. If your app uses WebSockets, add the `Upgrade` and `Connection` headers as in the [Node.js tutorial's nginx config](https://vpsnine.com/tutorials/deploy-nodejs-app-pm2-nginx-ubuntu).

Enable the site and reload nginx:

```bash
sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
```

Check it worked: the API answers through nginx, and FastAPI's interactive docs load:

```bash
curl -H "Host: api.example.com" http://127.0.0.1/items/7
curl -s -H "Host: api.example.com" http://127.0.0.1/docs | grep "<title>"
```

The second command prints `<title>FastAPI - Swagger UI</title>`. If you don't want the docs public, create the app with `FastAPI(docs_url=None, redoc_url=None)`.

## Open the firewall and add HTTPS

If ufw is active, allow web traffic (and keep port 8000 closed):

```bash
sudo ufw allow "Nginx Full"
```

On VPSNine, a server with a [panel firewall](https://vpsnine.com/help/server-firewall) also needs TCP 80 and 443 allowed. Then follow [Serve a site with nginx and HTTPS](https://vpsnine.com/help/nginx-and-https) for the certificate: `certbot --nginx -d api.example.com` adds HTTPS to the `server` block above and renews it by itself.

## Deploy new code

Update the code, install any new dependencies and reload:

```bash
cd /srv/api/app
sudo -u api git pull
sudo -u api /srv/api/.venv/bin/pip install -r requirements.txt
sudo systemctl reload api
```

`reload` replaces the workers one set at a time, so requests in flight finish. Use `sudo systemctl restart api` instead after changing `gunicorn.conf.py` or upgrading Gunicorn itself.

## Troubleshooting

**`ModuleNotFoundError: No module named 'main'` (or `'app'`).** Gunicorn looks for the module relative to its working directory. With the layout here, run it from `/srv/api` (the service's `WorkingDirectory`) and name it `app.main:app`, not `main:app`.

**`Error: class uri 'uvicorn_worker.UvicornWorker' invalid or not found`.** The `uvicorn-worker` package isn't installed in the venv the service uses. Install it with `/srv/api/.venv/bin/pip install uvicorn-worker`.

**`502 Bad Gateway` from nginx.** Gunicorn isn't running or listens elsewhere. `systemctl status api` and `journalctl -u api -n 50` show why it stopped; `/var/log/nginx/error.log` shows `connect() failed (111: Connection refused) while connecting to upstream`.

**Workers keep restarting with `WORKER TIMEOUT` in the log.** A request took longer than `timeout` seconds, usually a blocking call (a sync database driver, `requests`, `time.sleep`) inside an `async def` endpoint. Make the endpoint a plain `def` so FastAPI runs it in a thread, or use an async library.

## Next steps

- Give the app a database: [Install PostgreSQL on Ubuntu 24.04](https://vpsnine.com/tutorials/install-postgresql-ubuntu), or [Redis](https://vpsnine.com/tutorials/install-redis-ubuntu) for caching and background jobs.
- Several apps on one server: [Caddy as a reverse proxy for several apps](https://vpsnine.com/tutorials/caddy-reverse-proxy-multiple-apps).
- Keep an eye on memory as you add workers: [Monitor your server](https://vpsnine.com/help/monitor-your-server).
- References: [FastAPI deployment concepts](https://fastapi.tiangolo.com/deployment/concepts/), [Gunicorn settings](https://docs.gunicorn.org/en/stable/settings.html), [uvicorn-worker](https://github.com/Kludex/uvicorn-worker).
