October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PCOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
VGSources
Blog

How to Create a FastAPI App Using Docker Compose on a VPS (with HTTPS)

A production-focused walkthrough for running FastAPI with Docker Compose on a VPS, including a private API network, Caddy HTTPS, environment files, scaling, Gunicorn choices, and troubleshooting.
Length10 min Posted Quest giverVGSources Team
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Deploy FastAPI on a VPS by running the API in one Docker Compose service and putting a TLS reverse proxy such as Caddy in front of it. The proxy publishes ports 80 and 443, while FastAPI listens only on the private Compose network. This guide builds that layout, adds automatic HTTPS, keeps secrets outside the image, and gives you repeatable update and troubleshooting procedures.

The production layout

A small VPS deployment needs four pieces: your FastAPI code, an image that contains its runtime dependencies, a Compose file that defines the containers, and a reverse proxy that terminates HTTPS. The proxy is the only service exposed to the public internet. It forwards requests over Docker’s private network to the API service.

As an Amazon Associate I earn from qualifying purchases.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Component Public exposure Responsibility
FastAPI container None; internal port 8000 Runs the application process and returns API responses
Caddy container TCP 80 and 443 Obtains and renews certificates, redirects HTTP to HTTPS, and proxies requests
Docker Compose VPS local control socket Creates the network, starts services, injects environment variables, and restarts containers
DNS Your domain’s public A record Points the hostname to the VPS address so certificate validation and client traffic reach the proxy

The official FastAPI Docker guidance recommends one Uvicorn process per container and scaling by running more containers when you need more capacity. That model is the default in this tutorial. Running several worker processes inside one container is an alternative only when you have a specific reason to do so.

Prerequisites and decisions

  • A VPS with a public IPv4 address and a Linux distribution supported by Docker Engine.
  • A domain name whose DNS A record can be pointed at that address. Use an AAAA record as well if you deliberately support IPv6.
  • Docker Engine and the Docker Compose plugin installed on the VPS. Confirm both with docker --version and docker compose version.
  • SSH access with permission to install packages, create files, and run Docker.
  • Inbound TCP 80 and 443 allowed by the VPS provider and the operating-system firewall. Port 22 must remain available for your SSH administration.
  • Your FastAPI project arranged so that the importable application object is available as app.main:app.

Use a current Docker Engine and Compose plugin from your distribution or Docker’s installation instructions. Package names and commands differ between Debian-based, Red Hat-based, and other VPS images, so do not blindly copy an installation command for a different distribution.

Create the FastAPI application

Minimal application file

Create app/main.py with a health endpoint and one example route. A health endpoint gives you a simple target for deployment checks and monitoring.

from fastapi import FastAPI

app = FastAPI(title='VPS API')

@app.get('/healthz')
def healthz():
    return {'status': 'ok'}

@app.get('/items/{item_id}')
def read_item(item_id: int):
    return {'item_id': item_id}

Keep your actual dependencies in requirements.txt. At minimum, a synchronous example needs FastAPI and an ASGI server; add database drivers, authentication libraries, and other packages your application imports.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
fastapi
uvicorn[standard]

Build the image with a Dockerfile

Place this Dockerfile beside requirements.txt. It installs dependencies in a separate layer, copies the application, and starts the FastAPI CLI on the internal port used by Compose.

FROM python:3.12-slim

WORKDIR /code

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app ./app

EXPOSE 8000
CMD fastapi run app/main.py --proxy-headers --port 8000

The --proxy-headers option is important when a TLS-terminating proxy sits in front of FastAPI: it allows the framework to interpret forwarded scheme and client information correctly. The official example uses the FastAPI CLI and port 80; this deployment uses port 8000 so that the proxy can own the host’s standard web ports. The port number is internal and can be changed as long as the Compose service and proxy agree.

Keep the build context clean

Add a .dockerignore file so local virtual environments, Git metadata, caches, and secrets are not sent to the Docker daemon.

.git
.venv
__pycache__
*.pyc
.env
.env.*

Never copy a production .env file into the image. Images are commonly cached, exported, and inspected by more people than the running container.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Define the Compose services

Create compose.yaml with one proxy and one API service. Only Caddy publishes host ports; the API uses expose, which documents its internal port without opening it on the VPS.

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

  api:
    build: .
    restart: unless-stopped
    env_file:
      - .env
    expose:
      - '8000'

volumes:
  caddy_data:
  caddy_config:

restart: unless-stopped brings either container back after a crash or VPS reboot, but an intentional docker compose stop remains stopped. env_file injects values at runtime and keeps them out of the Dockerfile and image layers. Values under an inline environment section override entries loaded from the file, which is useful for a small deployment-specific adjustment.

Create the environment file

Create .env on the VPS and restrict its permissions to the account that operates the stack. Use real values for your application; the names below are examples.

APP_ENV=production
DATABASE_URL=postgresql://api_user:[email protected]/app
SECRET_KEY=replace-with-a-long-random-value

Do not commit this file to source control. If your application needs a database, cache, or object storage, provide its connection details through this file or through the VPS’s secret-management facility. The Compose file shown here does not create a database, so the hostname in the example must refer to a service or managed database that actually exists.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Configure HTTPS with Caddy

Write the Caddyfile

Create a file named Caddyfile beside compose.yaml. Replace api.example.com with the hostname you will publish in DNS.

api.example.com {
    reverse_proxy api:8000
}

Caddy listens on both published ports, obtains a certificate for the hostname, redirects ordinary HTTP requests to HTTPS, and forwards HTTPS requests to the Compose service named api. The name api is resolved on the private Compose network; it is not a public DNS name.

The named volumes are essential. Caddy stores certificate data under /data and configuration state under /config. Without persistent volumes, recreating the container can discard state and trigger avoidable certificate requests.

Point DNS before starting the proxy

  1. Create an A record for the exact hostname in the Caddyfile and point it to the VPS’s public IPv4 address.
  2. If an AAAA record exists, make sure the VPS really accepts IPv6 traffic on ports 80 and 443; otherwise remove the stale record while troubleshooting.
  3. Wait for DNS to resolve publicly, then verify that the VPS firewall and provider security rules allow ports 80 and 443.

Certificate issuance can fail when the hostname resolves elsewhere, a firewall blocks the validation request, or another service is already bound to port 80 or 443. Fix DNS or port ownership before repeatedly restarting Caddy.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Deploy on the VPS

  1. Install Docker Engine and the Compose plugin using instructions for the VPS distribution. Verify the versions with docker --version and docker compose version.
  2. Copy the project to the VPS with Git, an archive transfer, or your deployment system. The directory should contain Dockerfile, requirements.txt, app/main.py, compose.yaml, and Caddyfile.
  3. Create .env on the VPS. Keep it outside the repository if your deployment process might overwrite tracked files.
  4. Check the rendered Compose configuration before starting anything:
docker compose config

This catches malformed YAML, missing environment substitutions, and indentation errors. It does not prove that the application can connect to every external dependency.

  1. Build and start the stack in detached mode:
docker compose up -d --build
  1. Inspect service state and logs:
docker compose ps
docker compose logs --tail=100 api
docker compose logs --tail=100 caddy
  1. After DNS has propagated and Caddy reports a successful certificate, request the health endpoint over HTTPS:
curl -i https://api.example.com/healthz

A successful response should show an HTTP 200 status and the JSON body from healthz. Do not test the API by publishing 8000:8000 on the host; doing so bypasses the proxy and exposes an endpoint that was intended to stay private.

Understand the process model and scaling choices

One Uvicorn process per container

The recommended baseline is one Uvicorn process in each API container. It keeps process supervision simple, makes container CPU and memory usage easier to observe, and lets the orchestrator add or remove identical replicas. For a single small VPS, start with one replica and measure before adding complexity.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Scale with additional containers

When your workload needs more concurrency, build an image that is safe to run repeatedly and scale the API service:

docker compose up -d --scale api=3

Multiple replicas require a proxy configuration that can discover and distribute traffic among them, plus enough CPU and memory on the VPS. Confirm how your chosen proxy resolves multiple Compose endpoints before relying on this command in production. Stateful in-memory sessions, local file uploads, and background jobs need a shared design before replicas are safe.

Use Gunicorn only for an explicit multi-worker requirement

Gunicorn can manage several worker processes inside one container with the Uvicorn worker class. The common pattern is:

gunicorn app.main:app -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000

The 0.0.0.0 bind address is required for traffic arriving from outside the container. A third-party deployment guide notes that the uvicorn.workers module is being deprecated in favor of a standalone uvicorn-worker package; that transition is not confirmed here against Uvicorn’s own documentation, so verify the supported worker package and version before changing your image. Do not add Gunicorn merely because an older template includes it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Choose a reverse proxy

Proxy Best fit Configuration style Operational trade-off
Caddy One or a few domains on a VPS A short Caddyfile; automatic certificate management Lowest setup effort, with state stored in mounted volumes
Traefik Many Compose services and frequently changing routes Docker labels and entrypoint configuration Label-driven discovery is powerful but requires careful router and certificate-resolver setup
Nginx Teams that want manual, explicit proxy and static-file rules Hand-written configuration files Maximum directive-level control, but certificate renewal and configuration maintenance are more manual

Traefik considerations

A Traefik deployment normally mounts the Docker socket read-only, uses labels to define routers, and disables automatic exposure with exposedByDefault set to false. Only a service labelled traefik.enable=true is then routed. A typical HTTPS setup also defines a websecure entrypoint and an ACME certificate resolver that writes its state to an acme.json file. Treat Traefik version numbers and label syntax as configuration inputs to verify against the version you install; examples using the v3 line are not proof that every later release uses identical defaults.

When Nginx is appropriate

Nginx is a reasonable choice when you already maintain Nginx configuration, need detailed static-file handling, or want every proxy rule reviewed as a file change. The deployment pattern remains the same: publish 80 and 443 from Nginx, keep FastAPI on the private network, and pass the original scheme and host in forwarded headers so the application can generate correct HTTPS URLs.

Updates, logs, and recovery

Apply an application release

  1. Copy or pull the new source revision into the project directory.
  2. Run docker compose config to validate the merged configuration.
  3. Rebuild and recreate the API container with docker compose up -d --build api.
  4. Watch startup logs and call /healthz before sending real traffic.

The Caddy container does not need to be recreated for an application-only change. Recreate it when you change the Caddyfile or the proxy image.

Inspect failures without guessing

  • docker compose ps shows whether a service exited or is restarting.
  • docker compose logs -f api follows application startup, import, and connection errors.
  • docker compose logs -f caddy reveals certificate, DNS, bind, and upstream errors.
  • docker compose exec api opens a shell in the running API container for checking files and environment names without printing secret values.

Rollback a bad image

Keep the previous source revision or image tag available. If a release fails its health check, stop sending traffic by correcting the proxy route or stopping the new API container, restore the previous revision, and run docker compose up -d --build api again. Do not delete Caddy’s persistent volumes during an application rollback; they contain certificate state unrelated to the API code.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshooting by symptom

Symptom Likely cause What to check
502 Bad Gateway from the proxy The API is stopped, listening on another port, or not reachable as api Run docker compose ps, inspect API logs, and confirm the Dockerfile port, expose value, and Caddy upstream all say 8000.
Certificate issuance fails DNS still points elsewhere, ports 80/443 are blocked, or another process owns a port Resolve the hostname from outside the VPS, inspect firewall rules, and check listening sockets before restarting Caddy.
FastAPI reports an import error The module path or copied directory does not match app.main:app Confirm app/main.py exists in the image and defines a variable named app.
HTTPS redirects loop or generated URLs use HTTP Forwarded scheme headers are missing or not trusted Keep --proxy-headers enabled and verify that the proxy passes the original host and scheme.
Secrets are missing at runtime The .env file is absent, misnamed, unreadable, or uses names the code does not read Run docker compose config, check the service’s env_file, and inspect variable names without echoing secret values.
Changes disappear after a reboot Data was written to a container filesystem or the service lacks a restart policy Use persistent volumes for required state and keep restart: unless-stopped on long-running services.
Port binding error during startup Another web server or old container already uses 80 or 443 Stop the conflicting service or change its ownership of the ports; only one proxy can bind each host port.

Security and maintenance checklist

  • Expose only the proxy’s 80 and 443 ports. Avoid publishing the API’s 8000 port.
  • Keep .env, private keys, and certificate data out of Git and out of image layers.
  • Use read-only mounts where possible, including the Caddyfile and a Docker socket mount for Traefik.
  • Pin and periodically update base images and Python dependencies; rebuild to receive security fixes.
  • Review logs for accidental credentials before sending them to a third-party service.
  • Back up application data and any external database separately from the containers. Recreating a container is not a database backup.
  • Test certificate renewal and a full VPS reboot during a maintenance window, not for the first time during an outage.
  • Keep an earlier image or source revision so a failed deployment can be reversed quickly.
  • Apply the VPS provider’s SSH and firewall hardening guidance; the Compose file cannot secure the host itself.

What this setup gives you

With this arrangement, Docker builds a repeatable FastAPI image, Compose keeps the proxy and API together, Caddy owns public HTTPS, and the application remains isolated on an internal network. The design follows FastAPI’s one-process-per-container guidance, leaves room to scale API containers when necessary, and avoids the deprecated all-in-one Uvicorn/Gunicorn image pattern. Start with one API container, confirm DNS, certificates, logs, and health checks, then introduce replicas or Gunicorn only when an observed workload justifies the additional operational complexity.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More quests from Patch Notes

  1. How to Set Up a RedM (RDR2) Server in 2026: License Key, txAdmin and server.cfg, Step by StepBlog11min
  2. Best RedM (RDR2) Server Hosting in 2026: Comparing Four Hosts on Slots, Memory and PriceBlog10min
  3. How to Host a Mindustry Server in 2026: server-release.jar, Port 6567 and the Commands That MatterBlog7min
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.