DEV Community

Sarah Thomas
Sarah Thomas

Posted on

Deploy a FastAPI App on a VPS with Docker Compose: A Beginner-Friendly Guide

Running your own Python application on a VPS can be a great way to learn how servers, containers, databases, domains, and HTTPS work together.

It also gives you more control over your application. Instead of depending entirely on a managed hosting platform, you can deploy your application on your own virtual server and decide how the environment is configured.

In this tutorial, we'll deploy a small FastAPI application on a Linux VPS using:

FastAPI for the web application
PostgreSQL for the database
Docker for containerization
Docker Compose for managing services
Caddy as the reverse proxy and HTTPS layer
A container registry for storing application images

The goal isn't to build a huge production infrastructure. Instead, we'll create a setup that is simple enough for beginners to understand and useful enough for small projects, APIs, SaaS prototypes, and side projects.

Who is this guide for?

This guide is useful if you:

want to learn how VPS deployment works
are building a small Python API
want to understand Docker in a real project
want more control over your hosting environment
are experimenting with self-hosting
want to deploy a FastAPI project without using a fully managed platform

It may not be the best approach if you need:

automatic scaling from day one
zero server maintenance
enterprise-grade infrastructure
a managed database
a large distributed system

For a small application, however, a single VPS can be surprisingly capable.

What we'll build

At the end of this guide, the request flow will look roughly like this:

User
|
| HTTPS
v
Caddy
|
| HTTP
v
FastAPI
|
| PostgreSQL connection
v
PostgreSQL

Everything except the VPS itself will run through Docker containers.

This is one of the useful things about Docker Compose: instead of manually installing every dependency on the server, you can describe the services your application needs in a Compose file and manage them together. Docker describes Compose as a way to define and run multi-container applications across development, testing, staging, and production environments.

Prerequisites

Before starting, you should have:

A working FastAPI application
A Linux VPS
A domain name
Basic knowledge of the terminal
SSH access to your VPS
Docker installed locally
A container registry account

You don't need to be a DevOps expert.

If you understand basic Linux commands such as cd, ls, mkdir, and ssh, you can follow along.

1. Prepare the FastAPI application

Let's assume our project has a structure similar to this:

my-fastapi-app/
├── app/
│ ├── init.py
│ └── main.py
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
└── .env

A very simple FastAPI application could look like this:

from fastapi import FastAPI

app = FastAPI()

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

And our requirements.txt:

fastapi
uvicorn[standard]

You can add other packages later as your project grows.

For example:

fastapi
uvicorn[standard]
sqlalchemy
psycopg[binary]
python-dotenv

The important thing is that the application works locally before we introduce the VPS.

2. Create a Dockerfile

Now we need to package the application.

Create a file named:

Dockerfile

Add:

FROM python:3.12-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

COPY requirements.txt .

RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
What is happening here?

The Dockerfile starts with a lightweight Python image.

Then:

/app becomes the working directory.
Python environment variables are configured.
Dependencies are installed.
Application files are copied into the image.
Port 8000 is exposed.
Uvicorn starts the FastAPI application.

The important idea is that the server doesn't need to know how you originally built your Python environment.

The container contains what the application needs.

3. Add Docker Compose

Our FastAPI application needs a database, so let's create:

docker-compose.yml

A simple setup:

services:
api:
build: .
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- db

db:
image: postgres:17
restart: unless-stopped
env_file:
- .env
volumes:
- postgres_data:/var/lib/postgresql/data

volumes:
postgres_data:

Now we have two services:

api

This is our FastAPI application.

db

This is PostgreSQL.

The named volume is important because database data shouldn't disappear simply because the PostgreSQL container is recreated.

4. Configure environment variables

Create a .env file:

POSTGRES_DB=myapp
POSTGRES_USER=myappuser
POSTGRES_PASSWORD=change-this-password

DATABASE_URL=postgresql://myappuser:change-this-password@db:5432/myapp

Don't commit real passwords or secret keys to a public Git repository.

Docker's documentation also recommends being careful with sensitive information stored in environment variables and considering secrets for more serious deployments.

For a production application, you should also generate a strong application secret rather than using an example value.

  1. Test everything locally

Before touching your VPS, test the setup on your own computer.

Run:

docker compose up --build

Docker will:

Build the FastAPI image.
Start the PostgreSQL container.
Start the API container.
Create the Docker network connecting them.

Then open:

http://localhost:8000

You should see:

{
"message": "Hello from FastAPI!"
}

You can also check FastAPI's automatically generated documentation:

http://localhost:8000/docs

This is one of my favorite parts of FastAPI for learning because you immediately get an interactive API interface.

If the application doesn't work locally, don't move to the VPS yet.

Fix the local setup first.

6. Build the Docker image

Once the application works, build the production image:

docker build -t my-fastapi-app .

You can check that the image exists with:

docker images

At this stage, you have a portable application image.

That image can be stored in a container registry and later pulled by your VPS.

7. Push the image to a container registry

You have several choices for storing Docker images.

For this example, we'll use GitHub Container Registry.

GitHub's Container Registry supports Docker and OCI images and allows images to be stored under personal accounts or organizations.

First authenticate:

echo $CR_PAT | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin

Then tag your image:

docker tag my-fastapi-app \
ghcr.io/YOUR_GITHUB_USERNAME/my-fastapi-app:latest

Push it:

docker push \
ghcr.io/YOUR_GITHUB_USERNAME/my-fastapi-app:latest

For real projects, I recommend using meaningful version tags instead of relying only on latest.

For example:

v1.0.0
v1.1.0
v1.2.0

Or use a Git commit SHA.

That makes rollbacks much easier.

GitHub also supports pulling images by digest when you need an exact immutable image version.

8. Choose your VPS

For a small FastAPI project, you don't need a massive server.

A basic VPS can be enough for:

a small API
a PostgreSQL database
a few background processes
development projects
personal applications
small SaaS prototypes

The right VPS depends on your workload.

Consider:

RAM

Memory becomes important when you run several containers.

CPU

A small API usually doesn't need many CPU cores, but CPU-heavy applications may need more.

Storage

Remember that your database, logs, Docker images, and uploaded files all consume storage.

Location

Choose a data center reasonably close to your users when latency matters.

Backups

A VPS is not automatically a backup system.

This is an important distinction.

If your database exists only on one server and that server fails, you could lose your data.

9. Connect to the VPS

Once your server is ready, connect through SSH:

ssh root@YOUR_SERVER_IP

If your provider recommends connecting through a non-root user, use that account instead.

For better security, SSH keys are generally preferable to password-based login.

10. Update the server

On Ubuntu or another Debian-based system:

sudo apt update
sudo apt upgrade -y

Keeping the server updated is basic but important.

A server exposed to the public internet should not be treated like a temporary development machine.

11. Configure the firewall

For this setup, the public services generally need:

22 SSH
80 HTTP
443 HTTPS

Your application itself doesn't need to expose port 8000 publicly when Caddy is sitting in front of it.

That gives us:

Internet
|
+---- 80/443 ----> Caddy
|
+----> FastAPI :8000

Keeping internal application ports away from the public internet reduces unnecessary exposure.

12. Install Docker

Install Docker using the official instructions for your Linux distribution.

After installation, check:

docker --version

and:

docker compose version

If both commands work, you're ready for the deployment.

13. Prepare the production Compose file

Our local Compose configuration builds the application on the machine.

For production, it's often better to pull a previously built image.

Docker's production guidance recommends separating production-specific configuration where appropriate, including things such as ports, environment variables, restart policies, and additional services.

For example:

services:
api:
image: ghcr.io/YOUR_GITHUB_USERNAME/my-fastapi-app:latest
restart: unless-stopped
env_file:
- .env
expose:
- "8000"
depends_on:
- db

db:
image: postgres:17
restart: unless-stopped
env_file:
- .env
volumes:
- postgres_data:/var/lib/postgresql/data

caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- api

volumes:
postgres_data:
caddy_data:
caddy_config:

Notice something important:

We aren't publishing:

8000:8000

for the API.

Instead, Caddy can communicate with the API internally.

14. Configure Caddy

Create a file named:

Caddyfile

Add:

api.example.com {
reverse_proxy api:8000
}

Replace api.example.com with your actual domain.

Caddy acts as a reverse proxy.

The request flow becomes:

Browser
|
| HTTPS
v
Caddy
|
| HTTP inside Docker network
v
FastAPI
|
v
PostgreSQL

Caddy's reverse proxy can forward requests to backend services, and its HTTPS setup can automatically obtain publicly trusted certificates when the domain points correctly to the server and ports 80/443 are reachable.

This is one reason Caddy is convenient for small self-hosted projects.

15. Point your domain to the VPS

Go to your domain's DNS settings.

Create an A record:

Type: A
Name: @
Value: YOUR_SERVER_IP

If you're using a subdomain:

Type: A
Name: api
Value: YOUR_SERVER_IP

DNS changes can take some time to become visible depending on your DNS configuration.

Before starting Caddy, make sure the domain actually points to the correct server.

16. Log in to the container registry

If your image is private, your VPS needs permission to pull it.

For GitHub Container Registry:

echo $CR_PAT | docker login ghcr.io \
-u YOUR_GITHUB_USERNAME \
--password-stdin

GitHub's documentation notes that private packages require authentication and that a token needs appropriate package permissions.

Don't paste credentials directly into scripts that may become public.

17. Start the application

Now run:

docker compose up -d

Check the containers:

docker compose ps

You should see something similar to:

api
db
caddy

If something isn't working, inspect the logs:

docker compose logs api

For PostgreSQL:

docker compose logs db

For Caddy:

docker compose logs caddy

Logs are usually the first place I look when a containerized deployment doesn't behave as expected.

  1. Test your live API

Open:

https://api.example.com

You should receive the response from your FastAPI application.

Then try:

https://api.example.com/docs

If the FastAPI documentation appears, congratulations.

Your API is now running on your VPS behind HTTPS.

19. Deploying future updates

One of the benefits of using container images is that updating your application becomes straightforward.

After changing your code locally:

docker build -t my-fastapi-app .

Tag it:

docker tag my-fastapi-app \
ghcr.io/YOUR_GITHUB_USERNAME/my-fastapi-app:v1.1.0

Push it:

docker push \
ghcr.io/YOUR_GITHUB_USERNAME/my-fastapi-app:v1.1.0

Then update the image reference on your VPS.

For example:

image: ghcr.io/YOUR_GITHUB_USERNAME/my-fastapi-app:v1.1.0

Then:

docker compose pull api
docker compose up -d api

Docker documents this general pattern of rebuilding/pulling an updated image and recreating the affected service rather than unnecessarily restarting the entire stack.

20. Don't forget database migrations

This is an area where beginners often get surprised.

Updating application code is not always enough.

Suppose your application changes its database structure:

users

users + subscription_plan

Your new application code may expect a column that doesn't exist yet.

For a real FastAPI application using SQLAlchemy and Alembic, you would normally run migrations during deployment.

For example:

docker compose exec api alembic upgrade head

The exact command depends on how your application handles database changes.

The important lesson is:

Application deployment and database migration are related, but they are not the same operation.

Plan both.

21. Protect your production configuration

Never put credentials like these directly into your public repository:

POSTGRES_PASSWORD
DATABASE_URL
SECRET_KEY
API_KEYS

Use environment variables or a proper secrets system.

Also make sure .env is included in .gitignore:

.env

Django's official deployment checklist similarly emphasizes protecting secrets and production database credentials.

The exact framework differs, but the principle is the same:

If a secret doesn't need to be public, don't commit it to source control.

22. Add backups before you need them

A Docker volume isn't the same thing as a backup.

Our PostgreSQL data is stored in:

volumes:

  • postgres_data:/var/lib/postgresql/data

That's useful because the data survives container recreation.

But imagine the entire VPS disappears.

The volume disappears with it.

For anything important, create regular database backups and store them somewhere separate from the server.

A simple PostgreSQL backup might eventually look like:

pg_dump DATABASE_NAME > backup.sql

The exact backup strategy depends on your database setup, but the principle is simple:

Keep at least one copy somewhere outside the machine running your application.

23. Add monitoring as your project grows

You don't need a huge monitoring stack on day one.

Start with basic checks:

docker compose ps

and:

docker compose logs

As the application becomes more important, consider adding:

uptime monitoring
application error tracking
centralized logs
CPU and memory monitoring
database monitoring
alerts

This is where a small VPS deployment can gradually evolve into a more serious infrastructure setup.

24. Automate deployments with CI/CD

Manually running:

docker build
docker push
docker compose pull
docker compose up

every time you make a change becomes annoying quickly.

A better workflow is:

Git push

GitHub Actions

Build Docker image

Run tests

Push image

VPS pulls image

Restart API

GitHub's Container Registry can integrate with GitHub Actions, and GitHub recommends using GITHUB_TOKEN for workflows publishing packages associated with the repository.

You don't have to automate everything immediately.

Get the manual deployment working first.

Then automate the boring parts.

Common problems you may encounter
Container starts and immediately stops

Check:

docker compose logs api

Usually the error will tell you whether it's a missing dependency, invalid environment variable, incorrect module path, or application error.

Caddy returns a 502 error

A 502 generally means the proxy cannot successfully reach the backend.

Check:

docker compose ps

Then:

docker compose logs caddy

and:

docker compose logs api

Also verify that your Caddy configuration points to:

api:8000

rather than:

localhost:8000

Inside the Docker network, api is the service name.

HTTPS isn't working

Check three things:

Your DNS record points to the VPS.
Port 80 is reachable.
Port 443 is reachable.

Caddy's automatic HTTPS relies on the domain being correctly configured and publicly reachable.

The database disappears after a restart

Check your Compose file.

You should have a persistent volume:

volumes:

  • postgres_data:/var/lib/postgresql/data

Without persistent storage, recreating a database container can result in data loss.

And remember: persistent storage is still not a backup.

What we built

Let's recap the architecture:

                Internet
                   |
                   | HTTPS
                   v
              +---------+
              |  Caddy  |
              +---------+
                   |
                   | HTTP
                   v
              +---------+
              | FastAPI |
              +---------+
                   |
                   |
                   v
             +-----------+
             | PostgreSQL|
             +-----------+

      All services run with Docker Compose
Enter fullscreen mode Exit fullscreen mode

Our final stack contains:

FastAPI — application framework
Uvicorn — application server
PostgreSQL — database
Docker — container runtime
Docker Compose — service management
Caddy — reverse proxy and HTTPS
VPS — server infrastructure
Container Registry — application image storage
Final thoughts

Deploying a FastAPI application manually on a VPS can feel complicated the first time.

You have to understand several things at once:

Linux
SSH
Docker
networking
DNS
databases
HTTPS
containers
environment variables

But that's also why it's such a useful learning experience.

Once you understand how a request travels from:

Browser → DNS → VPS → Caddy → Docker → FastAPI → PostgreSQL

many modern deployment systems start making much more sense.

You don't need Kubernetes for every project.

You don't need a complicated cloud architecture for every API.

For a small application, a single VPS with Docker Compose can be a practical starting point. As your traffic and requirements grow, you can add managed databases, load balancing, monitoring, CI/CD, caching, multiple application servers, or other infrastructure when you actually need it.

Start simple.

Understand every layer.

Then scale the architecture when the application gives you a reason to.

Top comments (0)