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
| Item | Value |
|---|---|
| GCP Project ID | gen-lang-client-0380481603 |
| GCP Region | us-central1 |
| Docker Image | us-central1-docker.pkg.dev/gen-lang-client-0380481603/songbird/songbird-headless |
| Cloud Run Service | songbird-headless |
| Cloud Run URL | https://songbird-headless-6kdcepjumq-uc.a.run.app |
| GCS Bucket (projects) | gs://songbird-projects-gen-lang-client-0380481603 |
| GCS Bucket (binary) | gs://songbird-daw-club-repos |
| Binary path in GCS | gs://songbird-daw-club-repos/bin/<version>/SongbirdHeadless-release |
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.
| Resource | Name |
|---|---|
| Static IP | 34.111.95.214 |
| NEG | songbird-neg |
| Backend Service | songbird-backend |
| URL Map | songbird-lb |
| HTTP Proxy | songbird-http-proxy |
| HTTPS Proxy | songbird-https-proxy |
| SSL Certificate | songbird-cert (Google-managed for vm.daw.club) |
| Forwarding Rules | songbird-http-rule (port 80), songbird-https-rule (port 443) |
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:| Variable | Default | Description |
|---|---|---|
GCP_PROJECT_ID | gen-lang-client-0380481603 | GCP project ID |
GCP_REGION | us-central1 | Deployment region |
SERVICE_NAME | songbird-headless | Cloud Run service name |
GCS_BUCKET | songbird-projects-<project-id> | GCS bucket for project files |
BINARY_BUCKET | songbird-daw-club-repos | GCS bucket for prebuilt binaries |
SONGBIRD_API_KEY | (auto-generated) | API key for frontend authentication |
MEMORY | 2Gi | Container memory limit |
CPU | 2 | Container CPU limit |
MIN_INSTANCES | 0 | Minimum instances (0 = scale to zero) |
MAX_INSTANCES | 10 | Maximum instances |
DRY_RUN | 0 | Set to 1 to print commands without executing |
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
| Port | Protocol | Service |
|---|---|---|
| 8080 | HTTP | Nginx reverse proxy (Cloud Run ingress) |
| 9090 | WebSocket | Headless DAW engine |
| 9091 | HTTP | Headless DAW resource server (audio files, etc.) |
| 8765 | WebSocket | Collaboration server |
| 9092 | HTTP | Clone service (git clone API) |
/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:| Usage | Monthly Cost |
|---|---|
| Idle (0 instances) | $0 |
| 1 user, 8 hrs/day | ~$5-10 |
| 10 users, 8 hrs/day | ~$50-100 |
| Always-on (1 instance, min=1) | ~$30-50 |
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
Files Reference
| File | Purpose |
|---|---|
Dockerfile | Multi-stage build: C++ → React UI → runtime with nginx |
deploy/cloud-run/deploy.sh | One-command deployment script |
deploy/cloud-run/entrypoint.sh | Container entrypoint (process manager) |
deploy/cloud-run/nginx.conf | Reverse proxy with API key auth |
deploy/cloud-run/clone-service.js | Git clone HTTP API (clones repos to /tmp) |
deploy/cloud-run/service.yaml | Cloud Run Knative service definition (template) |
deploy/cloud-run/prebuilt/.keep | Placeholder for legacy Docker builder compat |
.dockerignore | Excludes .git, node_modules, build artifacts |