Fly.io
Deploy the rdio API and web app to Fly.io.
The repository includes Fly configuration for the API and web apps:
apps/api/fly.tomlapps/web/fly.toml
This is the maintained example deployment path for the repository, not a requirement. rdio can run on other platforms that support the production requirements in Deployment.
The API app is the main radio services app. It bundles the API, Liquidsoap, and Icecast, and mounts persistent media storage at /media.
1. Install the Fly CLI
Install and sign in with flyctl:
fly auth login2. Choose app names
Edit the app names in:
apps/api/fly.toml
apps/web/fly.tomlUse globally unique names. The examples use:
rdio-api
rdio-webIf you change the API app name, also update:
BROADCAST_HOSTinapps/api/fly.tomlapps/web/nginx.confproxy targetsBETTER_AUTH_URLandWEB_ORIGINsecrets
3. Create Postgres
Create a Fly Postgres database in the same region as the API app, then attach or copy its connection string into the API app as DATABASE_URL.
fly postgres createUse the database URL that Fly gives you as the API app's DATABASE_URL secret.
4. Set up Cloudflare R2
Create an R2 bucket and API token before deploying. See Cloudflare R2.
5. Create API storage
Create the API app and its media volume:
fly apps create <api-app-name>
fly volumes create rdio_media --app <api-app-name> --region <region> --size 10The API Fly config mounts this volume at /media.
6. Set API secrets
Set the required API secrets:
fly secrets set \
DATABASE_URL="<postgres-url>" \
BETTER_AUTH_SECRET="<strong-random-secret>" \
BETTER_AUTH_URL="https://<api-app-name>.fly.dev" \
WEB_ORIGIN="https://<web-app-name>.fly.dev" \
ICECAST_SOURCE_PASSWORD="<source-password>" \
R2_ACCOUNT_ID="<account-id>" \
R2_ACCESS_KEY_ID="<access-key>" \
R2_SECRET_ACCESS_KEY="<secret-key>" \
R2_BUCKET="<bucket-name>" \
R2_PUBLIC_URL="https://pub-example.r2.dev" \
--app <api-app-name>The API config sets BROADCAST_HOST for source clients. Make sure it points at the API app hostname.
7. Deploy the API
Deploy from the repository root so Docker can copy the workspace packages and Liquidsoap script:
pnpm deploy:apiOr manually:
fly deploy -c apps/api/fly.toml --local-only --depot=falseUse --local-only --depot=false if your Fly org does not have Depot builder access.
8. Run database migrations
After the first API deploy, run migrations against production Postgres:
DATABASE_URL="<postgres-url>" pnpm db:migrate9. Add fallback audio
Liquidsoap needs fallback audio when nothing is scheduled.
Place a file at:
/media/fallback/v1-tone.mp3Use fly ssh console --app <api-app-name> to upload or copy the file onto the mounted volume.
10. Deploy the web app
Update apps/web/nginx.conf if your API hostname differs from rdio-api.fly.dev, then deploy:
pnpm deploy:webThe web app expects API traffic at same-origin /api. Its nginx container proxies /api and /live.mp3 to the API app.
11. Finish setup
Open the web app:
https://<web-app-name>.fly.devThe first browser to complete setup creates the station administrator. See First-time setup.
12. Verify the deployment
Check these endpoints:
| Check | URL |
|---|---|
| API health | https://<api-app-name>.fly.dev/api/health |
| Stream | https://<api-app-name>.fly.dev/live.mp3 |
| Broadcast status | https://<api-app-name>.fly.dev/api/broadcast/status |
Confirm the web admin Settings page shows the correct stream URL.
Test harbor by connecting BUTT during a live broadcast slot. See Live broadcast.
Exposed ports
The API Fly config exposes:
| Port | Purpose |
|---|---|
3001 | API (internal; public via Fly HTTP) |
8001 | Icecast |
8005 | Harbor plain (BUTT) |
8443 | Harbor TLS (BUTT with SSL) |
Troubleshooting
If deploy or broadcast fails, see Troubleshooting.