Skip to content

Latest commit

 

History

History
161 lines (130 loc) · 11.1 KB

File metadata and controls

161 lines (130 loc) · 11.1 KB

Local dev runbook

How to lift, check, and tear down the full FUNK stack on a single machine.

All compose commands assume you're at the repo root: /home/furenku/punksnotdev/proyectos/FUNK/dev/FUNK.

Bring it up

0. One-time setup — shared dev network

The dev overrides attach auth and radio to a shared external bridge so the media plane's radio service can resolve auth by DNS name. Create it once:

docker network create funk_dev

(Prod does not use this — the planes live on separate hosts and the radio service reaches auth via a public HTTPS URL.)

1. Control plane — postgres, rustfs (object store), auth, storage

cp -n infra/env/control.dev.env.example infra/env/control.dev.env   # first time only
docker compose --env-file infra/env/control.dev.env \
  -f infra/compose/compose.control.yml \
  -f infra/compose/compose.control.dev.override.yml \
  up -d

2. Media plane — liquidsoap, icecast, HLS, nginx, radio

cp -n infra/env/media.dev.env.example infra/env/media.dev.env       # first time only
docker compose --env-file infra/env/media.dev.env \
  -f infra/compose/compose.media.yml \
  -f infra/compose/compose.media.dev.override.yml \
  up -d

When no schedule is applied, liquidsoap broadcasts white-noise fallback so HLS still has segments to serve. PUT a schedule via the radio API (next section) to play real audio.

3. Mint a credential and apply a test schedule

ADMIN_TOKEN=dev_admin_bootstrap_change_me   # matches control.dev.env.example

# Mint a service credential for the consumer. `scopes` is required — there is
# no implicit "everything" tier. See docs/CREDENTIAL_SCOPES.md.
TOKEN=$(curl -s -X POST http://localhost:7401/v1/credentials \
  -H "authorization: Bearer $ADMIN_TOKEN" \
  -H "content-type: application/json" \
  -d '{"label":"dev consumer","scopes":["radio:read","radio:schedule","storage:write"]}' | jq -r .token)

# Apply a one-entry schedule.
#
# ⚠ audio_url must be on the SCHEDULE_AUDIO_URL_ALLOWLIST, which defaults to
# FUNK's own storage service. Upload the file first and schedule the resulting
# key — an arbitrary origin like https://example.com/sample.mp3 is rejected 400.
KEY=$(curl -s -X POST http://localhost:7402/uploads \
  -H "authorization: Bearer $TOKEN" \
  -F "file=@./sample.mp3;type=audio/mpeg" | jq -r .key)

curl -s -X PUT http://localhost:7403/v1/radio/schedule \
  -H "authorization: Bearer $TOKEN" \
  -H "content-type: application/json" \
  -d "{\"entries\":[{\"audio_url\":\"http://storage:4002/files/$KEY\",\"title\":\"test\"}]}"

Endpoints

Host-published dev ports live in the reserved 74xx band so they never collide with other local stacks. Container-internal ports are unchanged; the table below shows the host (left) → internal mapping where they differ. The ports come from infra/env/*.dev.env (*_HOST_PORT).

Service URL Internal Notes
HLS playlist http://localhost:7488/hls/master.m3u8 8080 3-bitrate ladder
Icecast status http://localhost:7480 8000 Source mount: funk.mp3
Liquidsoap telnet localhost:7423 1234 echo help | nc 127.0.0.1 7423
Live harbor localhost:7481 8001 Mount: live
Breaking harbor localhost:7482 8002 Mount: breaking
Radio API http://localhost:7403 4003 Hono / Bun
Auth API http://localhost:7401 4001 Hono / Bun
Storage API http://localhost:7402 4002 Hono / Bun
Object store (S3) http://localhost:7490 9000 RustFS. S3 endpoint / presigned URLs
Postgres (host) localhost:7432 5432 Only exposed in dev override

Health check

docker ps --format 'table {{.Names}}\t{{.Status}}' | grep -E 'funk-(control|media)'
curl -s -o /dev/null -w "hls:     %{http_code}\n" http://localhost:7488/hls/master.m3u8
curl -s -o /dev/null -w "auth:    %{http_code}\n" http://localhost:7401/health
curl -s -o /dev/null -w "storage: %{http_code}\n" http://localhost:7402/health
curl -s -o /dev/null -w "radio:   %{http_code}\n" http://localhost:7403/health

Expect ~9 long-lived containers reporting healthy: 4 in control (postgres, rustfs, auth, storage; rustfs-init runs once and exits) and 5 in media (icecast, liquidsoap, hls, nginx, radio).

Tear down

# Control plane
docker compose --env-file infra/env/control.dev.env \
  -f infra/compose/compose.control.yml \
  -f infra/compose/compose.control.dev.override.yml \
  down

# Media plane
docker compose --env-file infra/env/media.dev.env \
  -f infra/compose/compose.media.yml \
  -f infra/compose/compose.media.dev.override.yml \
  down

Add -v to either command to wipe volumes (postgres data, object-store contents, applied schedule, recordings).

-v also deletes control_minio_data, the pre-2026-08-14 MinIO volume kept as the rollback path for the RustFS swap. Drop -v if you still want that rollback.

Gotchas

  • The object store is RustFS, but its DNS name is still minio. MinIO was replaced by rustfs/rustfs:1.0.0-rc.1 on 2026-08-14. The compose service is named rustfs; it keeps minio as a network alias on both control_private and funk_dev, because consumers outside this repo bake http://minio:9000/... into playout URLs and deployed env vars. Use rustfs in anything new. Differences that bite:

    • No web console. RustFS 1.0.0-rc.1 serves the S3 API only; :9001 answers with S3 XML, and RUSTFS_CONSOLE_ENABLE=true does not change that. There is no localhost:7491 any more. Administer with mc:
      docker run --rm --network funk-control_control_private --entrypoint /bin/sh minio/mc:latest -c \
        'mc alias set s3 http://rustfs:9000 "$S3_ACCESS_KEY" "$S3_SECRET_KEY" && mc ls --recursive s3/wtda-media'
    • No default CORS policy. MinIO shipped a permissive one; RustFS ships none. A plain <audio src> tag is unaffected, but fetch(), hls.js and Web Audio reads of a presigned URL fail their preflight without one. rustfs-init applies a bucket CORS config from S3_CORS_ALLOWED_ORIGINS to every bucket it manages. Narrow that from * in prod.
    • mc mb --ignore-existing prints "Bucket created successfully" even when the bucket already exists. RustFS returns success rather than BucketAlreadyOwnedByYou. The call is still idempotent and does not touch objects — just do not read that line as proof of an empty store.
    • Health endpoint is /health, not /minio/health/live.
    • Verified equivalent to MinIO on 2026-08-14: SigV4 presigned GET (same-host, cross-host, https-signed/http-delivered, expiry, tamper-rejection, Range, response-content-type overrides), anonymous bucket policies, PutObject with x-amz-meta-*, ListObjectsV2, HeadObject, and mc mirror/mc diff.
    • Env vars are now S3_ACCESS_KEY / S3_SECRET_KEY / S3_HOST_PORT. The old MINIO_ROOT_USER / MINIO_ROOT_PASSWORD / MINIO_HOST_PORT names still work as a fallback, so an env file can be renamed separately from the image swap.
    • Deployed environments are not migrated. See RUSTFS_MIGRATION.md.
  • Presigned download URLs point at whatever S3_PUBLIC_ENDPOINT resolves to. Keep it set to a host the browser can reach (default http://localhost:7490 in dev). A SigV4 presigned URL is bound to the Host header it was signed for, so any proxy in front of the store must pass Host through unchanged. The URL scheme is not signed, so signing for https://… and terminating TLS in front of a plain-HTTP store works.

  • Scheduled programme audio must be fetchable from inside funk-media-liquidsoap-1. A presigned URL is signed for exactly one host, and host-published ports (localhost:74xx, the docker bridge gateway, the host LAN IP) are unreachable from containers on machines whose firewall blocks bridge→host traffic — that is the usual reason a schedule "applies" but listeners hear only the white-noise fallback. Two dev arrangements make playout URLs work:

    • STORAGE_PROXY_DOWNLOADS=true (control env) → GET /files/<key> streams the bytes through the storage API instead of 302-ing to a presigned MinIO URL, so http://storage:4002/files/<key> works from liquidsoap, the host, and browsers alike. Leave it false in prod, where the S3 endpoint is publicly reachable.
    • liquidsoap and the object store both join the shared external funk_dev bridge, so consumers keeping their own bucket here can hand FUNK plain http://minio:9000/<bucket>/<key> playout URLs. minio is a compatibility DNS alias for the rustfs service — see the object-store gotcha below. Diagnose with docker exec funk-media-liquidsoap-1 python3 -c "import urllib.request;print(urllib.request.urlopen('<url>',timeout=5).status)" — the image has python3 but no curl/wget.
  • Radio reaches auth via the shared funk_dev bridge (see "One-time setup" above). The host-gateway approach used previously broke on hosts whose firewall blocks bridge-to-host traffic; the shared external network is more portable.

  • Per-host harbor credentials are minted via POST /v1/radio/live/credentials (live) or POST /v1/radio/interrupt/live (breaking). The response includes username and password — pass both to your broadcasting tool as the icecast login. Each credential is validated per-connection via an HTTP callback into radio; revocation (DELETE /v1/radio/live/credentials/:id) takes effect immediately on the next connection attempt. There is no longer a shared harbor password — HARBOR_LIVE_PASSWORD and HARBOR_BREAKING_PASSWORD are gone. RADIO_INTERNAL_SECRET is the new shared secret used only on the internal media_private bridge between liquidsoap and radio.

  • HLS CORS headers live in infra/services/media-nginx/nginx.conf. Each regex location declares its own add_header block — nginx does not inherit. Editing the file requires docker restart funk-media-nginx-1; nginx -s reload does not pick up the change reliably.

  • demo/svelte-poc/ is a minimal, working contract-reference demo of the current FUNK contract: a SvelteKit server route holds FUNK_SERVICE_TOKEN and proxies /v1/radio/now-playing, while the browser plays the anonymous HLS stream directly. Run it with cd demo/svelte-poc && bun install && bun run dev (port 7270) after pasting a minted service token into its .env — see its README.md. Consumer-side code is still meant to live in a separate repo per ADR-003; this dir is kept only as a living example of the contract.