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
- API key authentication — All routes except
/healthzrequire a validSONGBIRD_API_KEYviaX-API-Keyheader 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:- daw.club opens
https://vm.daw.club?repo=<clone_url>&api_key=<key>in a new tab - The React UI loads and detects the
?repo=query parameter - React UI calls
POST /api/clonewith{ "url": "<clone_url>" }— the clone service finds the bare repo on the GCS Fuse mount at/data/repos/<owner>/<repo>.gitand does a fast local checkout to/tmp/songbird-session/ - React UI connects to the headless DAW via WebSocket (
/ws) - React UI calls
projectChoice("openRecent:<cloned_path>")to load the project
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 blocksallUsers 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
- GCP project with billing enabled
- gcloud CLI authenticated (
gcloud auth login) - APIs enabled:
- Git submodules initialized (the Docker build copies the full source tree):
Quick Deploy
- Create a GCS bucket for project storage (if it doesn’t exist)
- Grant the Cloud Run service account access to the bucket
- Build the Docker image via Cloud Build (~35 min for full C++ build)
- Deploy to Cloud Run with spot instances + GCS Fuse
- Set up API key authentication (auto-generates a key if
SONGBIRD_API_KEYis not set) - Attempt
allUsersIAM 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:Configuration
Override defaults via environment variables:
Example with custom settings:
API Key Authentication
Why API keys?
The GCP org policy blocksallUsers 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
- deploy.sh auto-generates a random key via
python3 -c 'import secrets; print(secrets.token_urlsafe(32))'ifSONGBIRD_API_KEYenv var is not set - The key is passed to the Cloud Run service as an environment variable
- entrypoint.sh validates the key at startup:
- Refuses to start if
SONGBIRD_API_KEYis empty (prevents'' = ''nginx bypass) - Refuses to start if key is the placeholder
REPLACE_WITH_API_KEY
- Refuses to start if
- entrypoint.sh passes the key to
envsubstwhich substitutes it into nginx.conf - nginx.conf checks every request (except
/healthz) for a valid key via:X-API-Keyheader?api_key=query parameter
- 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 withvalueFrom.secretKeyRef. - Nginx
ifdirectives don’t inherit into nestedlocationblocks — each location has its own auth check. - The
/healthzendpoint is intentionally unauthenticated (Cloud Run startup/liveness probes need it). - The
allUsersIAM 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: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 bydeploy.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 usee2-highcpu-8 machine type with 3600s timeout:
-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:--skip-build mode for future deploys.
Manual Deploy
1. Build the Docker image
2. Deploy to Cloud Run
Editdeploy/cloud-run/service.yaml:
- Update the
image:tag to your version - Update the
SONGBIRD_API_KEYvalue (do NOT leave it asREPLACE_WITH_API_KEY) - Update
bucketNameif 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. fromgcloud auth print-access-token):
gsutil does NOT pick up CLOUDSDK_AUTH_ACCESS_TOKEN. Always use gcloud storage
commands instead:
gcloud storage cpinstead ofgsutil cpgcloud storage lsinstead ofgsutil lsgcloud storage buckets createinstead ofgsutil mb
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
Theentrypoint.sh script starts services in this order:
- Validate API key — exit if empty or placeholder
- GCS Fuse mount — only for local Docker runs; Cloud Run uses CSI driver
- Xvfb — virtual framebuffer (JUCE/GTK needs a display even headless)
- SongbirdHeadless — the C++ binary (WS :9090, HTTP :9091)
- Collab server — Node.js WebSocket server (:8765)
- Clone service — Node.js HTTP server for git clone API (:9092)
- Wait for readiness — polls headless HTTP port for up to 15 seconds
- Nginx — reverse proxy with API key auth (:8080)
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_KEYis set correctly in the Cloud Run service config - Check you’re passing the key via
X-API-Keyheader or?api_key=query param - The
/healthzendpoint 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.objectAdminon 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-8for faster compilation - Build uses
-j2to 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=1to 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