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
