RunPod Bootstrap — manual setup runbook
Retired in BRI-439: the RunPod runtime is gone.
Pre-v2 setup; for current RunPod ops see runpod-v2-cutover.md (BRI-357).
RunPod Bootstrap — manual setup runbook
Companion to .agents/plans/2026-07-18-runpod-bootstrap.md. Walks through every manual step to get the first SA3 job dispatched and returning tracks.
Status (2026-07-18): Phases A–C are done. The endpoint is live and returning tracks —
seeapps/runpod/README.mdfor what was actually deployed (endpoint id, image tag, GPU,
region) and for the container gotchas discovered while building it. Phase D (web app wiring
and the end-to-end test) is still outstanding.Two corrections from doing it for real:
- The image is PyTorch-only; the TensorRT path was stripped because it cannot be built
without a GPU present (tracked in BRI-18). The “TRT only serves small-music” limitation
below therefore does not apply yet — the endpoint runsstable-audio-3-medium.- Measured cost is higher than the ~$0.01/job estimated below: a 3x30s job takes ~43s of
RTX 4090 time plus platform fee. Re-measure on a full[[3,180],[2,160]]job before
trustingRUNPOD_EST_COST_CENTS_PER_JOB.
Prerequisites
- RunPod account (https://www.runpod.io)
- Docker locally with buildx
- A container registry account (Docker Hub or GHCR recommended)
WORKER_SHARED_SECRETvalue (must match what’s on the VPS web app — ask yourself / check VPS env)- GlitchTip DSN — get from https://eu.glitchtip.com/brioso/settings/keys/ (placeholder only in
apps/runpod/.env.example)
Phases
- Phase A — RunPod account prep (parallelizable now, no code dependency)
- Phase B — Image build + push (after W1–W4 from the plan are merged)
- Phase C — Endpoint creation + first job (after Phase B + image pushed)
- Phase D — Web app wiring + verification
Phase A — RunPod account prep
A1. Add credits
Console → Billing → Add credits.
- Recommended starting balance: $50 (covers ~150 hr of RTX 4090 Community at $0.34/hr — plenty for first-week testing)
- Payment: credit card (Stripe), crypto, or wire for >$5k
- Enable Low balance alert at $20
- Optional: Auto-pay with $50 reload at $10 threshold
Default spend cap is $80/hr across all resources. Fine for now; raise via support ticket later.
Doc: https://docs.runpod.io/accounts-billing/billing
A2. Create network volume for HF cache
Console → Storage → Network Volumes → Create.
| Field | Value |
|---|---|
| Name | brioso-sa3-cache |
| Datacenter | Pick a US region with RTX 4090 availability (US-GA-2, US-OR-2, etc.) — note the region; the endpoint MUST be in the same region |
| Size | 100 GB (SA3 model ~6–10 GB + HF cache grows) |
Mount path inside container is automatic at /runpod-volume — do not override.
⚠️ Region locks the endpoint. Once a volume is attached, the endpoint can only spin up workers in that volume’s datacenter. Pick a region known to have RTX 4090 + A6000 inventory.
Cost: $0.07/GB/month ≈ $7/month for 100 GB.
Doc: https://docs.runpod.io/storage/network-volumes
A3. Pick a container registry
RunPod has no first-party registry. We use Docker Hub:
- Docker Hub:
docker.io/usebrioso/brioso-runpod:<tag>— auth viaDOCKER_USERNAME/DOCKER_TOKEN(root.env);build-and-push.shlogs in automatically.
For private images, you’ll need to register credentials in RunPod later (Phase C5).
Phase B — Image build + push (after W1–W4 merged)
B1. Authenticate to your registry
# Docker Hub — build-and-push.sh logs in automatically when
# DOCKER_USERNAME/DOCKER_TOKEN are set in root .env. Manual login only for one-off pushes:
# echo "$DOCKER_TOKEN" | docker login -u "$DOCKER_USERNAME" --password-stdin
B2. Build the image
From repo root:
docker build -f apps/runpod/Dockerfile.sa3 -t docker.io/usebrioso/brioso-runpod:$(git rev-parse --short HEAD) .
Expected: long build (~20–40 min on first run due to git clone of stable-audio-3 + TRT engine compile via install.sh). Subsequent builds cached.
B3. Push
docker push docker.io/usebrioso/brioso-runpod:$(git rev-parse --short HEAD)
# also tag :latest for convenience
docker tag docker.io/usebrioso/brioso-runpod:$(git rev-parse --short HEAD) docker.io/usebrioso/brioso-runpod:latest
docker push docker.io/usebrioso/brioso-runpod:latest
The image is private on Docker Hub, so wire up registry auth (Phase C5) before endpoints can pull it.
Phase C — Endpoint creation + first job
C1. Create the serverless endpoint
Console → Serverless → New Endpoint.
Container image
| Field | Value |
|---|---|
| Container Image | docker.io/usebrioso/brioso-runpod:<tag> (or :latest) |
| Registry Auth | required (image is private) — pick the auth created in C5 |
GPU Configuration
| Field | Value |
|---|---|
| GPU type | RTX 4090 first, RTX A6000 as fallback (select both, in priority order) |
| GPUs per worker | 1 |
Worker scaling
| Field | Value |
|---|---|
| Active workers (min) | 0 (scale-to-zero when idle) |
| Max workers | 3 |
Advanced
| Field | Value | Why |
|---|---|---|
| Idle timeout | 60s | Keep worker warm between burst requests |
| Execution timeout | 600s | Matches LEASE_SECONDS=600 in sweep.ts — do not change one without the other |
| FlashBoot | Enabled (default) | Sub-200ms revival for warm workers |
| Container disk | 20 GB | SA3 medium + headroom |
| Network Volume | attach brioso-sa3-cache |
Same region as volume |
Doc: https://docs.runpod.io/serverless/endpoints/endpoint-configurations
C2. Set environment variables on the endpoint
Endpoint → Settings → Environment Variables.
| Variable | Value | Secret |
|---|---|---|
JOBSTORE_API_URL |
https://usebrioso.com |
no |
WORKER_SHARED_SECRET |
(same value as VPS web app) | yes |
PHOENIX_COLLECTOR_ENDPOINT |
https://usebrioso.com/otel (OTLP forward route on the web app; auth’d via worker secret) |
no |
OTEL_RELAY_TOKEN |
(same value as WORKER_SHARED_SECRET — the route validates the bearer against it) |
yes |
TRACING_ENABLED |
true |
no |
HF_HOME |
/runpod-volume/huggingface-cache (already in Dockerfile — confirm only) |
no |
Doc: https://docs.runpod.io/serverless/development/environment-variables
C3. Capture endpoint ID
After creation, copy the Endpoint ID from the endpoint detail page URL or panel. Looks like abcd1234efgh. This becomes RUNPOD_ENDPOINT_ID on the web app.
C3a. Beta staging endpoint
beta.usebrioso.com (see deploy/README.md’s “Beta / staging environment”
section) gets its own duplicated medium endpoint, same image — sharing
prod’s endpoint would let a beta job’s GPU worker call back into the prod
app, since JOBSTORE_API_URL is set per-endpoint here, not in Infisical.
Repeat C1–C2 with:
| Variable | Value |
|---|---|
JOBSTORE_API_URL |
https://beta.usebrioso.com |
WORKER_SHARED_SECRET / OTEL_RELAY_TOKEN |
beta’s own value (staging Infisical env), not prod’s |
Put the resulting endpoint id in staging’s RUNPOD_ENDPOINT_ID_MEDIUM.
Leave RUNPOD_ENDPOINT_ID_SMALL_* unset in staging — runpodClient.ts
returns a clean 501 for unconfigured variants rather than throwing. Serverless
scales to zero, so an idle beta endpoint costs nothing.
C4. Generate API key
Console → Settings → API Keys → Create API Key.
| Field | Value |
|---|---|
| Name | brioso-webapp-runpod |
| Permissions | Restricted |
| Endpoint access | Grant to this endpoint only |
| GraphQL access | None |
New key is prefixed rpa_. Copy immediately — RunPod does not store it. This becomes RUNPOD_API_KEY on the web app.
Doc: https://docs.runpod.io/get-started/api-keys
C5. (Only if image is private) Register registry credentials
Console → Settings → Container Registry Auths → Create.
| Field | Value |
|---|---|
| Provider | Custom (for GHCR) or Docker Hub |
| Username | GitHub username or Docker Hub username |
| Password / token | GHCR PAT with read:packages, or Docker Hub access token |
Generates a containerRegistryAuthId. Attach it to the endpoint template under Advanced.
C6. Smoke test from the console
Endpoint → Requests tab → modify the JSON body:
{
"input": {
"slug": "smoke-test",
"prompt": "warm acoustic guitar loop, 90 bpm, gentle fingerpicking",
"customer_email": "smoke@brioso.co",
"batches": [[1, 30]],
"steps": 8,
"cfg_scale": 1.0,
"seed": 42
}
}
Click Run. Watch the Logs tab for:
- Container boot + TRT engine load (~30–60s cold start)
[runpod-handler]log lines fromhandler.py- Generation progress
- Upload to R2 via presigned URLs (requires web app’s
/jobs/{slug}/upload-urlsroute to accept the request — authenticate withWORKER_SHARED_SECRET)
Expected outcome: {"tracks": [{"idx": 0, "preview_key": "...", ...}]}. If you see {"error": "..."}, check Logs first, then GlitchTip for the captured exception.
C7. Smoke test via curl (alternative)
curl -X POST https://api.runpod.ai/v2/$RUNPOD_ENDPOINT_ID/runsync \
-H "Authorization: Bearer $RUNPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": {
"slug": "smoke-test",
"prompt": "warm acoustic guitar loop, 90 bpm",
"customer_email": "smoke@brioso.co",
"batches": [[1, 30]],
"steps": 8,
"cfg_scale": 1.0,
"seed": 42
}
}'
Doc: https://docs.runpod.io/serverless/endpoints/send-requests
Phase D — Web app wiring + verification
D1. Set env vars on the VPS web app
Add to the web app’s env (whatever mechanism — systemd EnvironmentFile, dotenv, etc.):
RUNPOD_API_KEY=rpa_...
RUNPOD_ENDPOINT_ID=abcd1234efgh
DISPATCH_WAIT_SECONDS=45
DISPATCH_QUEUE_DEPTH_THRESHOLD=5
RUNPOD_DAILY_SPEND_CAP_CENTS=2000
RUNPOD_EST_COST_CENTS_PER_JOB=15
Restart the web app. Dispatch sweep is triggered externally (systemd timer / GH Actions cron per apps/web/src/routes/dispatch/sweep.ts) — confirm that scheduler is running.
D2. Verify runpod_spend table exists on target DB
The migration SQL exists at packages/db/src/migrations/20260711092351_swift_mastermind/migration.sql but packages/db/src/migrations/meta/_journal.json is missing, so mise run db:migrate cannot apply it. Check the table:
# against your prod libsql/SQLite
sqlite3 $DATABASE_URL "SELECT name FROM sqlite_master WHERE type='table' AND name='runpod_spend';"
If empty, apply manually:
sqlite3 $DATABASE_URL < packages/db/src/migrations/20260711092351_swift_mastermind/migration.sql
Without this table, sweep.ts:25-42 throws on every dispatch attempt.
D3. End-to-end test
With the home worker stopped (so it doesn’t claim first):
- Create a job via the normal UI / API (cfg=1.0, sm-music — the defaults).
- Wait
DISPATCH_WAIT_SECONDS + ~30sfor the sweep to fire. - Sweep logs should show:
sweep started→dispatched to runpod. - RunPod logs should show the handler running.
- Job in DB should end in
state: "paid"withtrackspopulated. - R2 (brioso-jobs bucket) should have preview + master objects under the slug key namespace.
D4. Monitor spend
- Console → Billing → live balance + Billing Explorer for history.
- Per-endpoint cost: Console → Serverless → endpoint → cost/run.
- The
runpod_spendSQLite table records daily aggregated spend in cents — queryable from the web app.
At $0.34/hr (RTX 4090 Community) and ~60s per generation after warm-up, real cost per job is ~$0.01. The plan’s default RUNPOD_EST_COST_CENTS_PER_JOB=15 (15¢) is conservative on purpose — covers cold starts.
Rollback
- Stop the endpoint: Console → Serverless → endpoint → Stop. Workers spin down, no further cost. Already-running requests complete.
- Disable dispatch: unset
RUNPOD_API_KEYon the web app —runpodClient.ts:33-34returns 501, sweep marks job as failed-attempt-1 and the home worker can still claim it. - Revert code: W1–W4 are independent; revert any single one without affecting the others.
Known limitations (do not file as bugs)
- TRT only serves
stable-audio-3-small-music. Medium and SFX requests will be gated at dispatch (W2 in the plan) and routed to the home worker. Multi-variant TRT support is deferred untilinstall.sh --variant mediumis verified upstream. - PyTorchEngine fallback is fixed (W1) but rarely exercised — only triggers when cfg≠1.0 / init / inpaint / non-pingpong, none of which the product currently emits.
- OTel tracing is active.
init_obs("brioso-runpod")is called in the handler; spans export to Phoenix via thehttps://usebrioso.com/otelforward route (BRI-63). - Manual spans and
jobs.trace_idpropagation for RunPod jobs are deferred to Phase 8. RunPod-dispatched jobs will havetrace_id = NULLuntil then.
Doc links (canonical)
- Billing: https://docs.runpod.io/accounts-billing/billing
- Network volumes: https://docs.runpod.io/storage/network-volumes
- Endpoint config: https://docs.runpod.io/serverless/endpoints/endpoint-configurations
- Env vars: https://docs.runpod.io/serverless/development/environment-variables
- API keys: https://docs.runpod.io/get-started/api-keys
- Send requests: https://docs.runpod.io/serverless/endpoints/send-requests
- Pricing: https://www.runpod.io/pricing