Choosing the right hosting platform impacts cost, performance, and maintenance burden. This chapter explores various deployment strategies.
Functions-as-a-Service approach:
Best for: Variable traffic, low-maintenance requirements
| Platform | Free Tier | Cold Start | Database |
|---|---|---|---|
| Vercel | 100K req/mo | ~200ms | External |
| Netlify | 125K req/mo | ~200ms | External |
| AWS Lambda | 1M req/mo | ~300ms | DynamoDB/RDS |
| Cloudflare Workers | 100K req/day | <1ms | D1/KV |
Docker deployments:
Best for: Consistent environments, full control
| Platform | Free Tier | Min Cost | Features |
|---|---|---|---|
| Fly.io | 3 VMs | $0 | Global, auto-scale |
| Railway | $5 credit | ~$5/mo | Easy deploys |
| Render | Static only | $7/mo | Managed |
| Google Cloud Run | 2M req/mo | ~$0 | Auto-scale |
Self-managed servers:
Best for: Maximum control, predictable costs
| Provider | Min Cost | RAM | Location |
|---|---|---|---|
| Hetzner | €4/mo | 2GB | EU |
| DigitalOcean | $6/mo | 1GB | Global |
| Linode | $5/mo | 1GB | Global |
| Vultr | $5/mo | 1GB | Global |
comment-api/
├── api/
│ └── comments.py
├── requirements.txt
└── vercel.json
// vercel.json
{
"version": 2,
"builds": [
{
"src": "api/comments.py",
"use": "@vercel/python"
}
],
"routes": [
{
"src": "/api/(.*)",
"dest": "/api/comments.py"
}
],
"env": {
"DATABASE_URL": "@database-url",
"ALLOWED_ORIGINS": "@allowed-origins"
}
}
# Install Vercel CLI
npm i -g vercel
# Login and deploy
vercel login
vercel --prod
// src/index.js
import { Router } from 'itty-router';
const router = Router();
router.get('/api/comments', async (request, env) => {
const url = new URL(request.url);
const pageId = url.searchParams.get('page_id');
const { results } = await env.DB.prepare(
'SELECT * FROM comments WHERE page_id = ? AND status = ? ORDER BY created_at DESC'
).bind(pageId, 'approved').all();
return Response.json({ data: results });
});
router.post('/api/comments', async (request, env) => {
const body = await request.json();
const result = await env.DB.prepare(
'INSERT INTO comments (page_id, author_name, content, status) VALUES (?, ?, ?, ?)'
).bind(body.page_id, body.author_name, body.content, 'pending').run();
return Response.json({ success: true, id: result.lastRowId }, { status: 201 });
});
export default {
fetch: (request, env) => router.handle(request, env)
};
# wrangler.toml
name = "comment-api"
main = "src/index.js"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "DB"
database_name = "comments"
database_id = "your-database-id"
[vars]
ALLOWED_ORIGIN = "https://yourdomain.com"
# Install wrangler
npm install -g wrangler
# Login
wrangler login
# Create D1 database
wrangler d1 create comments
# Deploy
wrangler deploy
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY app/ ./app/
# Create non-root user
RUN useradd -m appuser && chown -R appuser /app
USER appuser
# Run
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/comments
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: comments
POSTGRES_PASSWORD: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
# fly.toml
app = "my-comment-api"
primary_region = "cdg"
[build]
dockerfile = "Dockerfile"
[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
[[services]]
protocol = "tcp"
internal_port = 8000
[[services.ports]]
port = 80
handlers = ["http"]
[[services.ports]]
port = 443
handlers = ["tls", "http"]
[env]
ENVIRONMENT = "production"
# Install flyctl
curl -L https://fly.io/install.sh | sh
# Login and launch
fly auth login
fly launch
# Create PostgreSQL
fly postgres create
# Attach database
fly postgres attach my-comment-api-db
# Deploy
fly deploy
# Scale to multiple regions
fly scale count 2 --region cdg,iad
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "Dockerfile"
},
"deploy": {
"numReplicas": 1,
"healthcheckPath": "/health",
"restartPolicyType": "ON_FAILURE"
}
}
# Install Railway CLI
npm install -g @railway/cli
# Login
railway login
# Initialize project
railway init
# Add PostgreSQL
railway add -p postgresql
# Deploy
railway up
Free tier: 500MB storage, 2GB bandwidth
# Connection
DATABASE_URL = "postgresql://postgres:[password]@db.[project].supabase.co:5432/postgres"
Free tier: 5GB storage, 1 billion row reads
# Connection with SSL
DATABASE_URL = "mysql://[user]:[password]@[host]/[database]?ssl=true"
Free tier: 8GB storage, 9 databases
import libsql_experimental as libsql
conn = libsql.connect("your-db.turso.io", auth_token="your-token")
Free tier: 512MB, auto-suspend
DATABASE_URL = "postgresql://[user]:[password]@[endpoint].neon.tech/[database]?sslmode=require"
Free tier: 10K commands/day
import redis
r = redis.from_url("rediss://default:[password]@[endpoint].upstash.io:6379")
Free tier: 30MB
r = redis.Redis(
host='[endpoint].redis.cache.windows.net',
port=6380,
password='[password]',
ssl=True
)
# Required
DATABASE_URL=postgresql://...
SECRET_KEY=your-super-secret-key-at-least-32-chars
# Optional
REDIS_URL=redis://...
ALLOWED_ORIGINS=https://yourdomain.com
AKISMET_API_KEY=...
TURNSTILE_SECRET=...
# Monitoring
SENTRY_DSN=https://...
@app.get("/health")
async def health_check():
try:
# Check database
await db.execute(text("SELECT 1"))
db_status = "ok"
except Exception:
db_status = "error"
try:
# Check Redis
await redis.ping()
redis_status = "ok"
except Exception:
redis_status = "error"
status = "healthy" if db_status == "ok" and redis_status == "ok" else "degraded"
return {
"status": status,
"database": db_status,
"cache": redis_status,
"timestamp": datetime.utcnow().isoformat()
}
import logging
import json
class JSONFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"timestamp": self.formatTime(record),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName
})
logging.basicConfig(
level=logging.INFO,
handlers=[logging.StreamHandler()],
)
logging.getLogger().handlers[0].setFormatter(JSONFormatter())
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r requirements.txt
- run: pytest
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Deploy to Fly.io
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: $
| Platform | Best For | Cost | Complexity |
|---|---|---|---|
| Vercel | Quick start | Free-$20 | Low |
| Cloudflare Workers | Global perf | Free-$5 | Medium |
| Fly.io | Full control | Free-$10 | Medium |
| VPS | Maximum control | $5-20 | High |
Recommendation for starting: Vercel + Supabase (both have generous free tiers).
Navigation: