nginx + Uvicorn + FastAPI + systemd

October 21st, 2023

A cheat-sheet on how to run a FastAPI application within Uvicorn as a asgi-server under the systemd control and integrate it with nginx. Some more comments below (privacy, performance).

Create a virtual environment. Prerequisite (ubuntu):

apt install python3-venv

Installation of Uvicorn and FastAPI:

mkdir <application_dir>
cd <application_dir>/
python3 -m venv .
. ./bin/activate
pip install uvicorn
pip install fastapi

Create an application, e.g. hello.py:

from fastapi import FastAPI
app = FastAPI()

@app.get('/')
async def hello():
    return {"message": "hello"}

Monitoring via systemd:

/etc/systemd/system/uvicorn.service

[Unit]
Description=uvicorn daemon
After=network.target

[Service]
Type=exec
User=...
Group=...
WorkingDirectory=<application_dir>
Environment=...
#ExecStart=<application_dir>/bin/uvicorn --uds <unix_socket_path> --reload --root-path <mount_path> hello:app
ExecStart=<application_dir>/bin/uvicorn --uds <unix_socket_path> --workers <number_of_workers> --root-path <mount_path> hello:app
KillMode=mixed
PrivateTmp=true

[Install]
WantedBy=multi-user.target

where:

Note: (TODO) access and error logs need to be configured here as well.

Activate and start the service:

systemctl enable --now uvicorn.service

Test the application (unix socket - Uvicorn - FastAPI):

curl -X GET --unix-socket <unix_socket_path> http://does-not-matter/

Integrate it with nginx:

http {

    upstream fastapi {
        server unix:<unix_socket_path>;
    }

}

server {

    location /... {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://fastapi/;

    }

}

Details on the proxy_pass:

Privacy

Consider blocking an automatically generated documentation of your application, e.g. by setting the three endpoints to None when instantiating a FastAPI application app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None). Another approach would be to restrict the access to these resources, like the one discussed on GitHub.

Performance

Given the application:

from fastapi import FastAPI
from time import sleep

app = FastAPI()

@app.get("/")
async def root():
    sleep(1)
    return {"message": "hello"}

And the time measuring command:

$ time -p { seq 1 200 | \
  xargs -I % -n1 -P10 \
  sh -c 'curl -s -o /dev/null -X GET --unix-socket <unix_socket_path> http://does-not-matter/; echo -n %,;'; echo done; }

where:

Note: don't forget the semicolons. They are really required after both: the last command of the sh -c block and the last command of the time -p {} argument.

Results:

Comparison to flask

Just after a few tests: I don't see huge differences. Actually, flask did provide a solution for injecting environment-specific data (like database credentials). It seems there is no solution like this in FastAPI:

  app.config.from_object('config')
  app.config.from_pyfile('config.py')

Instead, you may read such variables out of a file using additional code or libraries like pydantic as described in the offical docs.


Next: OpenVPN with global IPv6 addressing

Previous: Upload to S3 bucket from bash

Main Menu