Skip to main content

Songbird Headless DAW — Cloud Run Deployment

Deploy the Songbird headless DAW engine to Google Cloud Run with GCS Fuse for project storage and spot instances for lower cost.

Architecture

Key properties:
  • API key authentication — All routes except /healthz require a valid SONGBIRD_API_KEY via X-API-Key header or ?api_key= query param
  • Spot instances — ~60-91% cheaper than standard Cloud Run
  • GCS Fuse — Project files (.bird, audio samples) mounted as a filesystem
  • 1 user per instance — Each DAW session gets a dedicated container (concurrency=1)
  • Scale to zero — No cost when idle
  • Session affinity — Sticky WebSocket connections for the duration of a session
  • 60-minute timeout — Long-lived WebSocket sessions supported

Launch Flow (daw.club → VM)

When a user clicks Launch on a project at daw.club:
  1. daw.club opens https://vm.daw.club?repo=<clone_url>&api_key=<key> in a new tab
  2. The React UI loads and detects the ?repo= query parameter
  3. React UI calls POST /api/clone with { "url": "<clone_url>" } — the clone service finds the bare repo on the GCS Fuse mount at /data/repos/<owner>/<repo>.git and does a fast local checkout to /tmp/songbird-session/
  4. React UI connects to the headless DAW via WebSocket (/ws)
  5. React UI calls projectChoice("openRecent:<cloned_path>") to load the project
Why GCS Fuse? Gitea’s bare repos are stored in a GCS bucket (songbird-gitea-repos) that is mounted read-only in Cloud Run at /data/repos. The clone service reads directly from this mount — no network clone needed. Session isolation: Each user gets their own Cloud Run container (containerConcurrency: 1). When the user disconnects, the container eventually idles and is destroyed — all checkout data in /tmp is wiped automatically. The next user always gets a fresh container.

Gitea Setup (one-time)

On the Gitea VM (git.daw.club), configure Gitea to store repos on GCS:

Current Deployment State

Load Balancer (Public Access)

The GCP org policy blocks allUsers IAM binding, so a Global External Application Load Balancer provides public access. Cloud Run’s invoker IAM check is disabled (--no-invoker-iam-check), and nginx handles API key authentication inside the container. DNS Setup: Point vm.daw.club A record to 34.111.95.214. The SSL certificate will auto-provision once DNS is configured. Until then, HTTP access works at http://34.111.95.214. Verify after deploy:

Prerequisites

  1. GCP project with billing enabled
  2. gcloud CLI authenticated (gcloud auth login)
  3. APIs enabled:
  4. Git submodules initialized (the Docker build copies the full source tree):

Quick Deploy

This will:
  1. Create a GCS bucket for project storage (if it doesn’t exist)
  2. Grant the Cloud Run service account access to the bucket
  3. Build the Docker image via Cloud Build (~35 min for full C++ build)
  4. Deploy to Cloud Run with spot instances + GCS Fuse
  5. Set up API key authentication (auto-generates a key if SONGBIRD_API_KEY is not set)
  6. Attempt allUsers IAM binding (gracefully skipped if org policy blocks it)

Skip-Build Mode (~2 min instead of ~35 min)

If the C++ binary is already built and uploaded to GCS:
This downloads the prebuilt binary from GCS and only rebuilds the Docker image with the React UI.

Configuration

Override defaults via environment variables: Example with custom settings:

API Key Authentication

Why API keys?

The GCP org policy blocks allUsers IAM binding on Cloud Run services. Instead of relaxing the org policy, we use a shared secret (API key) validated by nginx at the proxy layer.

How it works

  1. deploy.sh auto-generates a random key via python3 -c 'import secrets; print(secrets.token_urlsafe(32))' if SONGBIRD_API_KEY env var is not set
  2. The key is passed to the Cloud Run service as an environment variable
  3. entrypoint.sh validates the key at startup:
    • Refuses to start if SONGBIRD_API_KEY is empty (prevents '' = '' nginx bypass)
    • Refuses to start if key is the placeholder REPLACE_WITH_API_KEY
  4. entrypoint.sh passes the key to envsubst which substitutes it into nginx.conf
  5. nginx.conf checks every request (except /healthz) for a valid key via:
    • X-API-Key header
    • ?api_key= query parameter
  6. Returns 403 {"error":"invalid api key"} on mismatch

Security considerations

  • The API key is stored as a Cloud Run env var (visible in GCP console and gcloud run services describe). For higher security, use Secret Manager with valueFrom.secretKeyRef.
  • Nginx if directives don’t inherit into nested location blocks — each location has its own auth check.
  • The /healthz endpoint is intentionally unauthenticated (Cloud Run startup/liveness probes need it).
  • The allUsers IAM binding attempt is still made but gracefully falls back if org policy blocks it.

Frontend integration

The daw.club React UI passes the API key when connecting:
Or via WebSocket header:

Build Architecture

The Dockerfile uses a multi-stage build:
  • builder-full: Ubuntu 22.04, installs build-essential + JUCE deps, runs cmake + make with -j2 (avoids OOM)
  • builder-prebuilt: Copies binary from deploy/cloud-run/prebuilt/SongbirdHeadless (placed there by deploy.sh --skip-build)
  • react-ui: Node 20, runs npm ci + npx vite build (skips tsc — pre-existing TS errors on headless branch)
  • runtime: Ubuntu 22.04 with nginx, Node.js 20, Xvfb, GCS Fuse, tini (PID 1)

Cloud Build

Full builds use e2-highcpu-8 machine type with 3600s timeout:
The C++ build uses -j2 parallelism to stay under memory limits. Total build time is ~35 minutes.

Binary upload to GCS

After a successful full build, the binary is extracted from the Docker image and uploaded to GCS:
This enables --skip-build mode for future deploys.

Manual Deploy

1. Build the Docker image

2. Deploy to Cloud Run

Edit deploy/cloud-run/service.yaml:
  • Update the image: tag to your version
  • Update the SONGBIRD_API_KEY value (do NOT leave it as REPLACE_WITH_API_KEY)
  • Update bucketName if using a different GCS bucket

3. Upload a project

4. Connect from the React UI

GCP Authentication Notes

When using short-lived access tokens (e.g. from gcloud auth print-access-token):
Important: gsutil does NOT pick up CLOUDSDK_AUTH_ACCESS_TOKEN. Always use gcloud storage commands instead:
  • gcloud storage cp instead of gsutil cp
  • gcloud storage ls instead of gsutil ls
  • gcloud storage buckets create instead of gsutil mb
The deploy.sh script already uses gcloud storage for this reason.

Container Ports

All traffic enters through port 8080 (nginx) and is routed internally:
  • /ws → headless DAW WebSocket (:9090) — requires API key
  • /resources/ → headless DAW HTTP resources (:9091) — requires API key
  • /collab → collaboration server (:8765) — requires API key
  • /api/clone → clone service (:9092) — requires API key
  • /healthz → health check (nginx responds directly) — no auth
  • / → React UI static files — requires API key

Container Startup Sequence

The entrypoint.sh script starts services in this order:
  1. Validate API key — exit if empty or placeholder
  2. GCS Fuse mount — only for local Docker runs; Cloud Run uses CSI driver
  3. Xvfb — virtual framebuffer (JUCE/GTK needs a display even headless)
  4. SongbirdHeadless — the C++ binary (WS :9090, HTTP :9091)
  5. Collab server — Node.js WebSocket server (:8765)
  6. Clone service — Node.js HTTP server for git clone API (:9092)
  7. Wait for readiness — polls headless HTTP port for up to 15 seconds
  8. Nginx — reverse proxy with API key auth (:8080)
If any child process exits, the container shuts down (Cloud Run will restart it).

Cost Estimate

With spot instances and scale-to-zero: Spot instances are ~60-91% cheaper than standard but may be preempted. Project state is safe on GCS.

Troubleshooting

Container won’t start

  • Check logs: gcloud run services logs read songbird-headless --region=us-central1
  • If you see FATAL: SONGBIRD_API_KEY is not set — the env var is missing from the service config
  • If you see FATAL: SONGBIRD_API_KEY is still set to the placeholder value — update the key in service.yaml
  • The headless binary needs Xvfb (virtual framebuffer) — included in the Docker image
  • GTK/WebKit runtime libs must be present — see Dockerfile runtime stage

403 on all requests

  • Verify SONGBIRD_API_KEY is set correctly in the Cloud Run service config
  • Check you’re passing the key via X-API-Key header or ?api_key= query param
  • The /healthz endpoint should always return 200 without auth — if it returns 403, the nginx config is wrong

GCS Fuse mount fails

  • Ensure the Cloud Run service account has storage.objectAdmin on the bucket
  • GCS Fuse requires gen2 execution environment (set in service.yaml)
  • Check mount logs in container stderr

WebSocket connection drops

  • Cloud Run has a 60-minute request timeout (configured in service.yaml)
  • Session affinity is enabled to maintain sticky connections
  • If using a custom domain, ensure the load balancer supports WebSocket upgrade

Cloud Build times out

  • Default timeout is 30 minutes — C++ builds need ~35 minutes
  • Always use --timeout=3600 (60 min) for full builds
  • Use --machine-type=e2-highcpu-8 for faster compilation
  • Build uses -j2 to avoid OOM — don’t increase without more memory

Audio dropouts

  • Increase CPU/memory: MEMORY=4Gi CPU=4 ./deploy/cloud-run/deploy.sh
  • Cloud Run gen2 has better CPU performance than gen1
  • Consider MIN_INSTANCES=1 to avoid cold starts during active use

Org policy blocks allUsers

  • This is expected. The API key approach works without public IAM binding.
  • The deploy script gracefully handles this: || echo "allUsers binding skipped"
  • Do NOT try to relax the org policy — use the API key instead

Files Reference