Skip to content

Deploy to DigitalOcean

DigitalOcean App Platform runs all three pieces in one app: the FastAPI container, a managed PostgreSQL, and the static app + landing. (Prefer a single server you control? See Self-Hosting for a droplet + Docker Compose.)

Outcome: API at api.yourdomain.com, app at app.yourdomain.com, landing at yourdomain.com.

1. Database

Create a Managed PostgreSQL database (Databases → Create) and copy its connection string for FS_DB_URL.

2. Backend (API)

  1. Create an App from your repo and add a Service built from backend/Dockerfile.
  2. Set environment variables (Configuration):
    • FS_DB_URL: the managed database connection string
    • FS_ENVIRONMENT=prod, FS_BASE_API_URL=https://api.yourdomain.com, FS_BASE_WEB_URL=https://app.yourdomain.com
    • FS_JWT_SECRET_KEY, FS_CRON_SECRET, plus Stripe/email keys
  3. Run migrations against the managed DB: ./sqitch.sh prod deploy (from a console or your machine, with the prod FS_DB_URL).
  4. Add the domain api.yourdomain.com to the service.

3. App + landing (static sites)

Add two Static Site components from the same repo:

  • frontend (frontend/): build npm run build; env VITE_API_BASE_URL=https://api.yourdomain.com; domain app.yourdomain.com.
  • landing (landing/): build npm run build; domain yourdomain.com.

Serving the app from a sub-path

To serve the app at yourdomain.com/app instead of app.yourdomain.com: App Platform routes components of one app by path. Put both static sites in the same app, set the frontend component's Route to /app (the landing keeps /), and set index.html as the frontend's catchall document. The frontend and backend settings that go with this are in Serving from a Sub-Path.

4. Wire it together

Point DNS at the App Platform domains, confirm the FS_BASE_* URLs match the live ones, add the Stripe webhook at https://api.yourdomain.com/webhooks/stripe (Billing & Subscriptions), and run the Security checklist.

Next steps

Security headers

App Platform Static Sites cannot set custom response headers, so the app's security headers are added at the CDN instead. Put the app behind Cloudflare (free) and set them there:

  1. Add app.yourdomain.com to a Cloudflare zone and point its DNS at the App Platform domain (proxied).
  2. In Cloudflare, go to Rules → Transform Rules → Modify Response Header and add one Set rule per header:

    Content-Security-Policy: default-src 'self'; connect-src 'self' https://api.yourdomain.com; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
    X-Content-Type-Options: nosniff
    Referrer-Policy: strict-origin-when-cross-origin
    Permissions-Policy: camera=(), microphone=(), geolocation=()
    
  3. Enable HSTS under SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS) rather than as a header rule.

Replace https://api.yourdomain.com in connect-src with your real API URL, or the browser will block the app from calling it. See Security.