...

How to Deploy a FastAPI Application on a VPS with PostgreSQL and Nginx

Martin Klein

Reading time 1 minute

A FastAPI application can be deployed on a VPS as a full-fledged production service: install Python and the required dependencies, connect PostgreSQL, configure migrations, run the application with Gunicorn/Uvicorn, and set it up as a systemd service. Nginx will act as a reverse proxy and handle external HTTP/HTTPS requests, and an SSL certificate can be issued through Let’s Encrypt.

In this guide, we will deploy a small FastAPI API, add a health endpoint and Swagger UI, configure secret storage, set up PostgreSQL integration, view logs, and restart the application after code updates. We will not use Docker or similar containerization tools; instead, we will do it the old-fashioned way, which will help clarify how the application, reverse proxy, database, and the system as a whole work.

Preparing the VPS

To deploy FastAPI, you need a Linux-based VPS with SSH access. This example uses Ubuntu, and the application will run alongside PostgreSQL and Nginx.

Connecting to the Server via SSH

Connect to the VPS via SSH. On Windows, you can use PowerShell or Windows Terminal: ssh [email protected]

When connecting for the first time, confirm that you want to add the server key to the list of trusted keys by entering yes, then enter the root user’s password.

After successful authentication, the remote server’s command prompt will appear.

Updating the System and Installing Python, PostgreSQL, and Nginx

First, update the package index and installed packages: sudo apt update && sudo apt upgrade -y

Next, install Python, the tools for creating a virtual environment, PostgreSQL, Nginx, and the additional packages required by the application:

sudo apt install -y python3 python3-pip python3-venv python3-dev postgresql postgresql-contrib nginx libpq-dev

PostgreSQL is available directly from the Ubuntu repositories and can run as a system service after installation.

Creating a FastAPI Application

We will place the application in a separate directory and isolate its Python dependencies from system packages using the venv virtual environment. This approach allows the required library versions to be installed specifically for the project without affecting the entire system.

Creating the Project and Virtual Environment

Create the application directory and change into it:

sudo mkdir -p /var/www/fastapi-app

sudo chown -R $USER:$USER /var/www/fastapi-app

cd /var/www/fastapi-app

Create a virtual environment: python3 -m venv venv

Activate it: source venv/bin/activate

After activation, (venv) will appear at the beginning of the terminal prompt.

Installing FastAPI, Uvicorn, Gunicorn, and dependencies

Upgrade pip: pip install –upgrade pip

Install FastAPI, the Uvicorn ASGI server, Gunicorn, the PostgreSQL driver, SQLAlchemy, and Alembic: pip install fastapi uvicorn gunicorn sqlalchemy psycopg2-binary alembic python-dotenv

Save the list of installed dependencies: pip freeze > requirements.txt

The requirements.txt file lets you install the same dependencies later when migrating or redeploying the application.

Creating the API and health endpoint

Create the main application file: nano main.py

Add the following code:

from fastapi import FastAPI

app = FastAPI(

title="FastAPI VPS Example",

version="1.0.0"

)

@app.get("/")

def root():

return {"message": "FastAPI is running"}

@app.get("/health")

def health():

return {"status": "ok"}

Save the file with Ctrl+O, press Enter, then close the editor with Ctrl+X.

For an initial check, start the application with Uvicorn: uvicorn main:app –host 0.0.0.0 –port 8000

Uvicorn will start the ASGI application on port 8000. For a production deployment, FastAPI supports running multiple worker processes, and later in this guide, process management will be handed over to systemd.

In another terminal, or directly on the server, you can check the health endpoint: curl http://127.0.0.1:8000/health

The API should return: {"status":"ok"}

Configuring PostgreSQL

The FastAPI application will require a dedicated PostgreSQL database and user. We will move the connection parameters out of the source code and into a .env file.

Creating a database and user

Switch to the PostgreSQL system user and open the psql console: sudo -u postgres psql

Create an application user with a password: CREATE USER fastapi_user WITH PASSWORD ‘StrongPassword123!’;

Create a database and make the new user its owner: CREATE DATABASE fastapi_db OWNER fastapi_user;

The CREATE ROLE/CREATE USER and CREATE DATABASE commands are used in PostgreSQL to create roles and individual databases.

Exit the PostgreSQL console: \q

You can test the connection with the following command: psql -h 127.0.0.1 -U fastapi_user -d fastapi_db

Enter the password you created earlier. After connecting successfully, exit with: \q

Connecting FastAPI to PostgreSQL

Change to the application directory: cd /var/www/fastapi-app

Create the database.py file: nano database.py

Add the SQLAlchemy configuration:

import os

from dotenv import load_dotenv

from sqlalchemy import create_engine

from sqlalchemy.orm import declarative_base, sessionmaker

load_dotenv()

DATABASE_URL = os.getenv("DATABASE_URL")

engine = create_engine(DATABASE_URL)

SessionLocal = sessionmaker(

autocommit=False,

autoflush=False,

bind=engine

)

Base = declarative_base()

SQLAlchemy creates a connection using the database URL; for PostgreSQL with the psycopg2 driver, the postgresql+psycopg2://… format is used.

Storing secrets in a .env file

Create a .env file: nano .env

Add the connection string: DATABASE_URL=postgresql+psycopg2://fastapi_user:[email protected]:5432/fastapi_db

Restrict access to the file: chmod 600 .env

Check the database connection from the virtual environment:

source venv/bin/activate

python -c “from database import engine; conn = engine.connect(); print(‘PostgreSQL connection OK’); conn.close()”

If the connection is successful, the following message will appear: PostgreSQL connection OK

Configuring migrations

We will use Alembic to manage the database schema. It allows you to store schema changes as a sequence of migrations and apply them when deploying or updating the application.

Installing and Configuring Alembic

Alembic was already installed with the application dependencies. From the project directory, with the virtual environment activated, initialize its configuration: alembic init migrations

The command will create the migrations directory and the alembic.ini file.

Create a simple model that we will later use to generate a migration: nano models.py

Add:

from sqlalchemy import Column, Integer, String

from database import Base

class Item(Base):

__tablename__ = "items"

id = Column(Integer, primary_key=True)

name = Column(String(255), nullable=False)

Now open the file: nano migrations/env.py

Find the line: target_metadata = None

Replace it with:

from database import Base

import models

target_metadata = Base.metadata

Also add the following at the beginning of the file:

import os

from dotenv import load_dotenv

load_dotenv()

After the line config = context.config, add config.set_main_option("sqlalchemy.url", os.environ["DATABASE_URL"])

This way, Alembic will use the same connection string from .env as the application.

Creating and applying a migration

Create the first migration: alembic revision –autogenerate -m "create items table"

The –autogenerate option lets Alembic compare the SQLAlchemy metadata with the current database schema and prepare the changes for the migration file.

Apply the migration: alembic upgrade head

Check the current status: alembic current

Then verify that the table has been created in PostgreSQL: psql -h 127.0.0.1 -U fastapi_user -d fastapi_db -c "\dt"

The list should include the items and alembic_version tables.

Testing FastAPI

Before configuring the application to run persistently, make sure that FastAPI works correctly with Uvicorn and that the created endpoints are accessible locally.

Starting the Application with Uvicorn

Go to the project directory and activate the virtual environment:

cd /var/www/fastapi-app

source venv/bin/activate

Start the application: uvicorn main:app –host 127.0.0.1 –port 8000

Here, main is the name of the main.py file, and app is the FastAPI object created in that file. For ASGI applications, FastAPI can run with Uvicorn as a separate server process.

After startup, the terminal will display a message indicating that Uvicorn is accepting connections on 127.0.0.1:8000.

Checking the health endpoint and Swagger UI

Without stopping Uvicorn, open a second SSH session and run: curl http://127.0.0.1:8000/health

The response should be: {"status":"ok"}

Next, check the root endpoint: curl http://127.0.0.1:8000/

Response: {"message":"FastAPI is running"}

FastAPI automatically generates interactive Swagger UI documentation. While the application is available only locally, you can check it from the server itself with the following request: curl -I http://127.0.0.1:8000/docs

We will open the full Swagger UI interface in a browser after configuring Nginx and enabling access via the domain.

After the check, stop Uvicorn with Ctrl+C.

Running FastAPI with Gunicorn and systemd

To keep the application running continuously, we will run it through Gunicorn with a separate Uvicorn worker and delegate process management to systemd. This will allow the API to start automatically on server startup and restart after failures.

Configuring Gunicorn with Uvicorn Worker

Install the uvicorn-worker package:

source /var/www/fastapi-app/venv/bin/activate

pip install uvicorn-worker

pip freeze > requirements.txt

The separate uvicorn-worker package provides an ASGI worker for running applications through Gunicorn. Using it lets Gunicorn continue managing processes while Uvicorn handles the ASGI application.

Check that the application starts:

gunicorn main:app \

–workers 2 \

–worker-class uvicorn_worker.UvicornWorker \

–bind 127.0.0.1:8000

The –workers 2 option starts two worker processes. Multiple workers allow several application processes to run concurrently.

In another SSH session, run: curl http://127.0.0.1:8000/health

After successful verification, stop Gunicorn with Ctrl+C.

Creating a systemd service

Create a unit file: sudo nano /etc/systemd/system/fastapi.service

Add the following:

[Unit]

Description=FastAPI application

After=network.target postgresql.service

[Service]

User=root

Group=www-data

WorkingDirectory=/var/www/fastapi-app

EnvironmentFile=/var/www/fastapi-app/.env

ExecStart=/var/www/fastapi-app/venv/bin/gunicorn main:app \

–workers 2 \

–worker-class uvicorn_worker.UvicornWorker \

–bind 127.0.0.1:8000

Restart=always

RestartSec=5

[Install]

WantedBy=multi-user.target

The application will accept requests only on the local address 127.0.0.1:8000. External access will be configured later via Nginx.

Starting the service and checking its status

Reload the systemd configuration: sudo systemctl daemon-reload

Enable the service to start on boot: sudo systemctl enable fastapi

Start it: sudo systemctl start fastapi

Check the status: sudo systemctl status fastapi –no-pager

If the service starts successfully, it should be in the following state: Active: active (running)

Additionally, check the API: curl http://127.0.0.1:8000/health

Response: {"status":"ok"}

Configuring Nginx

FastAPI is already running as a local service at 127.0.0.1:8000, but this port should not be exposed directly to the internet, and there is no need to do so. We will configure Nginx in front of the application to accept external requests and forward them to FastAPI as a reverse proxy. This setup follows the typical pattern of running FastAPI behind a proxy server.

Creating a reverse proxy for FastAPI

Create a separate site configuration file: sudo nano /etc/nginx/sites-available/fastapi

Add the following configuration:

server {

listen 80;

listen [::]:80;

server_name api.example.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;

}

}

The proxy_pass directive forwards requests from Nginx to the local FastAPI server.

Enable the configuration: sudo ln -s /etc/nginx/sites-available/fastapi /etc/nginx/sites-enabled/fastapi

If the default Nginx site is no longer in use, disable it: sudo rm -f /etc/nginx/sites-enabled/default

Test the configuration: sudo nginx -t

If there are no errors, apply the changes: sudo systemctl reload nginx

Testing the API through the domain

By this point, the domain’s DNS record should point to the VPS’s public IP address. For example, for api.example.com, create an A record pointing to the server address.

After the DNS records have been updated, test the API: curl http://api.example.com/health

Expected response: {"status":"ok"}

You can also open the following URL in a browser: http://api.example.com/docs

If proxying is configured correctly, the FastAPI Swagger UI will load.

Configuring HTTPS

The next step is to enable HTTPS. The TLS connection will be terminated at Nginx, while FastAPI will continue running locally behind the reverse proxy. This setup eliminates the need to configure certificates directly within the application.

Installing Certbot

Install Certbot and the Nginx plugin:

sudo apt update

sudo apt install -y certbot python3-certbot-nginx

Verify that Certbot is available: certbot –version

Obtaining a Let’s Encrypt SSL Certificate

Run Certbot for the application domain: sudo certbot –nginx -d api.example.com

Enter your email address, accept the terms of service, and choose to redirect HTTP traffic to HTTPS if Certbot offers that option.

The Nginx plugin allows Certbot to obtain the certificate and automatically update the web server configuration to use HTTPS.

After the certificate is issued, check the configuration: sudo nginx -t

Then test the automatic renewal process: sudo certbot renew –dry-run

Testing the API over HTTPS

Check the health endpoint: curl https://api.example.com/health

Expected response: {“status”:”ok”}

Swagger UI is now available at the secure URL: https://api.example.com/docs

Application Logs and Maintenance

When operating an API, it is important to be able to quickly check the application status and identify the cause of an error. For this purpose, you can use the systemd journal and the standard Nginx logs.

Viewing FastAPI logs with journalctl

Because the application runs as a systemd service, you can view its output with journalctl. This command is used to read entries from the systemd journal.

To view the most recent FastAPI log entries, run: sudo journalctl -u fastapi -n 50 –no-pager

To view the log in real time: sudo journalctl -u fastapi -f

This will show messages from Gunicorn and Uvicorn, application startup errors, and other information written by the service.

To exit real-time viewing mode, press Ctrl+C.

Viewing Nginx Logs

You can view the Nginx access log with the following command: sudo tail -n 50 /var/log/nginx/access.log

Nginx errors are logged separately: sudo tail -n 50 /var/log/nginx/error.log

To monitor errors in real time: sudo tail -f /var/log/nginx/error.log

Updating the FastAPI Application

After changing the source code, the application must be restarted so that the Gunicorn worker processes load the new version. If the update affects the database schema, the new migrations should also be applied before restarting.

Updating the Code and Applying Migrations

Change to the project directory: cd /var/www/fastapi-app

Activate the virtual environment: source venv/bin/activate

After replacing the source code or obtaining a new version, install the dependencies from requirements.txt if they have changed: pip install -r requirements.txt

If the new application version includes Alembic migrations, apply them: alembic upgrade head

You can check the currently applied migration with the following command: alembic current

Restarting the systemd service

To have Gunicorn load the updated version of the application, restart the service: sudo systemctl restart fastapi

Check its status: sudo systemctl status fastapi –no-pager

The service should be back in this state: Active: active (running)

Verifying the application after the update

Check the health endpoint at the public HTTPS address: curl https://api.example.com/health

Expected response: {"status":"ok"}

If necessary, you can check the application log immediately after the restart: sudo journalctl -u fastapi -n 30 –no-pager

This lets you verify that the new FastAPI version started successfully and did not exit with an error after the update.

Conclusion

The FastAPI application has been deployed on a VPS and connected to PostgreSQL. Alembic is used to manage the database schema, and the application is run by Gunicorn with Uvicorn Worker and operates as a systemd service with automatic startup after a server reboot.

Nginx accepts external requests and forwards them to FastAPI via a reverse proxy, while Certbot enables the API to run over HTTPS. This configuration also makes it possible to store secrets centrally in .env, view logs with journalctl, and quickly restart the application after code updates.

FAQ

Why Does FastAPI Need Nginx?

FastAPI can be run directly on an ASGI server, but for a public deployment, Nginx is convenient to use as a reverse proxy. It accepts external HTTP and HTTPS requests, handles the TLS certificate, and forwards requests to the local application. The official FastAPI documentation also covers deploying an application behind a proxy server.

Where should the PostgreSQL password and other secrets be stored?

Passwords, tokens, and connection strings should not be placed directly in the source code. In the configuration discussed, they are stored in the .env file, with access restricted by Linux file system permissions. When using a version control system, this file should also be added to .gitignore.

What are Alembic migrations used for?

Migrations let you update the database schema incrementally as the application evolves. For example, after adding a new column to a SQLAlchemy model, you can create a new migration and apply it to PostgreSQL instead of modifying the tables manually.

Where is the FastAPI Swagger UI located?

FastAPI automatically provides interactive Swagger UI documentation. With the default settings, after deployment, it is available at: https://api.example.com/docs

Swagger UI lets you view the available endpoints and request parameters, and send test requests to the API.

How can you check whether FastAPI is running after a server restart?

Check the status of the systemd service: sudo systemctl status fastapi

Then send a request to the health endpoint: curl https://api.example.com/health

If the API is running normally, it will return: {“status”:”ok”}

How do I restart FastAPI after changing the code?

After updating the application files, run: sudo systemctl restart fastapi

If the database schema changed along with the code, apply the prepared migrations before restarting: alembic upgrade head

Sources

  1. FastAPI Documentation — Deployment
  2. PostgreSQL Documentation — CREATE DATABASE, CREATE ROLE
  3. Nginx Documentation — ngx_http_proxy_module
  4. Certbot — Nginx Instructions

Subscribe to our newsletter and receive articles and news

    Check out our other materials