Complete DevOps Bootcamp: Master DevOps in 12 Weeks
FastAPIDeployment and Production

Deploying on VPS and Cloud Platforms

You have an image. You have a database. Now you need a place where they can run, on the internet, with a real URL. There are a lot of choices, and the best one depends less on technical capability than on how much operational work you actually want to own.

This page surveys the realistic options, with honest trade-offs. There is no "best" - there's "best for your team and your stage."

The hosting spectrum

   more you manage  ────────────────────────────────►  less you manage
   ─────────────────────────────────────────────────────────────────────
   bare metal       VPS         IaaS         PaaS         Serverless
   ─────────────────────────────────────────────────────────────────────
   colocation       Hetzner     AWS EC2      Render        AWS Lambda
                    DigitalOcean GCP Compute  Fly.io        Cloudflare Workers
                    Linode      Azure VM     Railway       Vercel
                                              Heroku
                                              Koyeb

Cost generally goes up as you move right, but so does the time you reclaim. A team of two shouldn't be running Kubernetes; a team of fifty shouldn't be ssh'ing into individual VPSes.

Option 1: A single VPS (DigitalOcean, Hetzner, Linode, etc.)

The "you own the box" approach. Best when:

  • You're early, simple, and want to keep costs at $5-$20/month.
  • You want to learn how the stack actually works.
  • You don't mind being on the hook for OS updates and the occasional 2 AM page.

The flow is roughly:

   1. provision a VM (Ubuntu LTS is fine)
   2. SSH in, install Docker and docker-compose
   3. copy your compose file, set up a .env with prod secrets
   4. run `docker compose up -d`
   5. point your DNS at the VM's IP
   6. put nginx in front for TLS (next page covers this)

A minimal production docker-compose.yml:

services:
  api:
    image: yourorg/yourapp:${TAG}
    restart: always
    env_file: .env.prod
    ports:
      - "127.0.0.1:8000:8000"     # only bind to localhost; nginx is in front
    depends_on:
      db: { condition: service_healthy }

  db:
    image: postgres:16
    restart: always
    environment:
      POSTGRES_USER_FILE: /run/secrets/db_user
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: app
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      retries: 5
    secrets:
      - db_user
      - db_password

volumes:
  db_data:

secrets:
  db_user:
    file: ./secrets/db_user
  db_password:
    file: ./secrets/db_password

The deploy script is laughably small:

#!/bin/bash
# deploy.sh
set -e
TAG=$(git rev-parse --short HEAD)
docker build -t yourorg/yourapp:$TAG .
docker push yourorg/yourapp:$TAG
ssh deploy@yourserver "cd /srv/app && TAG=$TAG docker compose up -d"

What you sign up for: backups (you set them up), TLS renewal (certbot, but you watch it), OS patches (you apt upgrade periodically), monitoring (you install something). All learnable. All work.

Option 2: A platform-as-a-service (Render, Fly.io, Railway, Heroku, Koyeb)

The "git push and it's live" approach. Best when:

  • You'd rather pay $20-100/month than spend an evening every two weeks on ops.
  • Your team is small and you want one fewer thing to think about.
  • The platform's pricing matches your traffic profile.

The shape:

   1. connect your git repo
   2. point at a Dockerfile (or let it infer Python)
   3. set environment variables in the dashboard
   4. push to main → it deploys

A render.yaml example for the Render platform:

services:
  - type: web
    name: api
    runtime: docker
    plan: starter
    dockerfilePath: ./Dockerfile
    healthCheckPath: /healthz/live
    envVars:
      - key: DATABASE_URL
        fromDatabase: { name: app-db, property: connectionString }
      - key: SECRET_KEY
        generateValue: true
      - key: ENVIRONMENT
        value: production

databases:
  - name: app-db
    plan: starter
    postgresMajorVersion: 16

What the platform handles for you: TLS, deploy pipelines, log aggregation, simple metrics, database backups, scaling between instances, zero-downtime deploys. What you give up: fine control, predictable cost at scale, the ability to do anything too unusual.

Fly.io specifically is worth a mention for FastAPI because it gives you Dockerfiles + a CLI + global edge deployment without much fuss. A fly.toml is essentially a deployment manifest:

app = "my-fastapi-app"
primary_region = "iad"

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = 8000
  force_https = true
  auto_stop_machines = true
  min_machines_running = 1

[[http_service.checks]]
  interval = "30s"
  timeout  = "5s"
  method   = "get"
  path     = "/healthz/live"

fly deploy does the rest.

Option 3: Managed Kubernetes (EKS, GKE, AKS)

Best when:

  • You're already running several services and the operational maturity of Kubernetes pays back.
  • Your team has the headcount and skills to run it.
  • You need fine control over scaling, networking, and multi-region.

Worst when:

  • You're a small team treating Kubernetes as a status symbol rather than a tool.

For a FastAPI app, the rough shape is a deployment, a service, an ingress, and a config + secret object. A minimal deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2
  selector:
    matchLabels: { app: api }
  template:
    metadata:
      labels: { app: api }
    spec:
      containers:
        - name: api
          image: yourorg/yourapp:abc1234
          ports:
            - containerPort: 8000
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef: { name: api-secrets, key: database_url }
            - name: SECRET_KEY
              valueFrom:
                secretKeyRef: { name: api-secrets, key: secret_key }
          readinessProbe:
            httpGet: { path: /health, port: 8000 }
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet: { path: /healthz/live, port: 8000 }
            initialDelaySeconds: 30
            periodSeconds: 30
          resources:
            requests: { cpu: "100m", memory: "256Mi" }
            limits:   { cpu: "500m", memory: "512Mi" }

The honest take: don't pick Kubernetes for a single FastAPI service. Pick it when you already have five services to coordinate and the leverage is real.

Option 4: Serverless (AWS Lambda + API Gateway, Cloudflare Workers)

Best when:

  • Traffic is spiky and you don't want to pay for idle capacity.
  • Cold-start latency is acceptable for your use case.
  • The fit with your code is genuine (small, stateless, fast).

FastAPI runs on Lambda via Mangum:

from mangum import Mangum
from app.main import app

handler = Mangum(app)

Then you package the dependencies, ship to Lambda, and put API Gateway in front. The operational concerns get smaller but new ones appear: cold starts (1-3 seconds for a Python app with imports), 15-minute execution limit, no long-lived connections (WebSockets need special handling), debugging is different.

For genuinely sporadic APIs (admin endpoints called twice a day, webhooks), Lambda is great. For anything with steady traffic, a regular server is almost always cheaper and faster.

How to decide

A small flowchart:

   Do you have <10 paying users / no revenue yet?

        ├── yes ──► single VPS or a free-tier PaaS. Spend zero time on infra.

        └── no

            ├── Is your team < 5 engineers?
            │   ├── yes ──► PaaS (Render, Fly, Railway). Pay for time, not iron.
            │   └── no

            ├── Do you have multiple services already?
            │   ├── yes ──► managed Kubernetes is genuinely the right tool
            │   └── no    ──► PaaS still wins unless you have a specific reason

            └── Is your traffic extremely spiky (idle most of the time)?
                    └── consider serverless for the spiky bits, regular hosting for the rest

The default for most projects: PaaS. The exception: when you've outgrown it for a specific, measurable reason.

A few cross-cutting concerns

Whatever you pick, a handful of things matter everywhere.

Database - almost always managed

Self-hosting Postgres on the same VM as your app is fine for very early stage. The moment it matters, move to a managed database (RDS, Cloud SQL, Render Postgres, Supabase, Neon, etc.). Backups, point-in-time recovery, failover, patching - none of that is your job anymore.

The cost difference is real ($15-30/mo for a small managed DB vs. "free" on your VM), but the time saved the first time you actually need backups pays for years of it.

TLS - never DIY

Use Let's Encrypt via certbot if you're on a VPS. Use the platform's built-in TLS if you're on a PaaS. Never run your own CA. Never use self-signed certs in production. Browsers and clients are increasingly hostile to anything that isn't a proper public TLS cert.

DNS and CDN

Cloudflare (free tier) in front of almost any deployment is a low-cost win. You get DDoS protection, basic WAF, edge caching, and DNS in one place. Just be aware that proxied DNS hides your origin IP - which is what you want for security, but trips people up when they're debugging.

Deploy automation

Don't ssh in to deploy. Even if you start by hand, write the deploy as a script the same day. CI that builds and pushes the image, then SSH'es or kubectl-applies the new tag, is fifteen lines of YAML.

# .github/workflows/deploy.yml (skeleton)
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t yourorg/yourapp:${{ github.sha }} .
      - run: docker push yourorg/yourapp:${{ github.sha }}
      - run: ssh deploy@server "TAG=${{ github.sha }} docker compose -f /srv/app/compose.yml up -d"

A pricing sanity check

A rough order-of-magnitude for what hosting costs in 2025:

SetupTypical monthly
Hetzner / DigitalOcean VPS + managed DB$20-50
Fly.io / Render small app + DB$25-100
Heroku / similar with the polish tax$50-200
AWS Fargate + RDS + ALB (medium)$150-500
Managed Kubernetes (EKS/GKE) with multiple services$300+

Prices move; ratios don't, much. The "right" tier is the one you can pay for now while still being able to grow into the next one.

A trap to avoid

Picking infrastructure for a hypothetical future. "We might need to scale to a million users so let's set up Kubernetes." If you don't have a million users today, you don't have a million users' worth of operational maturity either, and Kubernetes will eat the time you should be spending on the things that get you to that million.

A boring, simple deployment that you can actually maintain beats a glamorous, complex one you can't. Always.

What's next

Whichever platform you pick, you almost always end up with a reverse proxy in front of your app - for TLS, for static files, for routing, for many small operational reasons. The next page covers nginx specifically, because it's the proxy you'll meet most often whether you chose it or not.

How is this guide?

Last updated on