cloud-run
Deploy headless robotics / sim / demo containers to Google Cloud Run: the build → Artifact Registry → Cloud Run path plus the gotchas that bite sim workloads (no UDP multicast for gz-transport/DDS, CPU allocated only while a request is open, session affinity for per-visitor insta
Install
npx skills add https://github.com/robium-ai/robium/tree/main/archive/cloud-run/1.0.2
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install robium-ai-robium@llmmart
git clone https://github.com/robium-ai/robium.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole robium-ai/robium collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
cloud-run
The deploy target for robium's headless robotics and demo containers: a sim + bridge (or a policy server) built into an image, pushed to Artifact Registry, and run on Google Cloud Run so a browser can reach it, optionally one private instance per visitor, scale-to-zero when idle. This skill consolidates the Cloud-Run-specific mechanics and the robotics/headless gotchas that were hard-won deploying a Gazebo Harmonic + Nav2 stack (robium's nav-trial demo, 2026-07-12/13). It stays thin on general gcloud/GCP usage (that lives in Google's own docs) and embeds only what a robotics container hits that a plain web service never does.
When to use this skill
- Building and deploying any headless robotics/sim/demo container to Cloud Run:
the
gcloud/ Cloud Build → Artifact Registry →gcloud run deploypath, and the flag set a sim workload needs (CPU/memory, timeout, concurrency, affinity). - Debugging a robotics container that runs locally but misbehaves on Cloud Run: gz/DDS discovery silent (no multicast), sim boot freezes (CPU throttled between requests), WebSocket drops, affinity cookie ignored, deploy fails on VPC IPs.
- Wiring deploy auth from CI/servers with a GCP service-account key.
- The trigger phrases in the description: 'deploy to Cloud Run', 'gcloud run deploy', 'Cloud Build', 'Artifact Registry', 'session affinity', '--no-cpu-throttling', 'GZ_RELAY on Cloud Run', 'Cloud Run WebSocket timeout'.
- Cross-references: go to the sibling skill instead when the question is:
- The demo orchestrator, session gateway (claim/status/shutdown + ws tunnel),
mission-control page, or viewer handoff →
live-demo(it uses this skill's deploy mechanics but owns everything demo-shaped above the container). - Whether to use uv vs Docker, local↔remote reproducibility, GPU passthrough,
or GPU-cloud/RunPod provisioning →
environments(Cloud Run is CPU-only and is one deploy target it points at; RunPod is the GPU one). foxglove_bridgemechanics, thefoxglove.sdk.v1subprotocol, MCAP → thefoxgloveskill (this skill only covers reaching the bridge through Cloud Run's proxy).- The whole-stack decision and where deploy sits →
architect(routes here).
- The demo orchestrator, session gateway (claim/status/shutdown + ws tunnel),
mission-control page, or viewer handoff →
Key directives
- Delegation posture: embed + links. The robotics/headless Cloud Run
gotchas (no multicast → unicast relay, CPU-on-request throttling, same-site
affinity, VPC subnet sizing, long-ws timeout/concurrency) are embedded here
because they exist nowhere upstream in one place; they were derived by
deployment. For general gcloud/GCP mechanics (auth, project setup, the full
gcloud run deployflag reference, Cloud Build, Artifact Registry repo creation) point upstream to Google's docs; don't restate them from memory. - Scale-to-zero, and state per-session cost.
--min-instances=0unless a warm floor is explicitly justified. The billing-mode choice (request-based vs--no-cpu-throttling) is a cost decision: state it and its per-session figure in the app/demo docs, and put a GCP budget alert on the project. - A boot that needs CPU must hold a request while it boots. Cloud Run's
default (request-based) billing allocates CPU only while a request is open.
A robotics container that boots a sim in the background freezes the moment no
request is in flight. Either drive the boot with a held connection (the
viewer's WebSocket does this) or pick
--no-cpu-throttling. - No UDP multicast anywhere on Cloud Run. gz-transport and DDS discovery both assume multicast; neither gets it. Force unicast/loopback (see Quick start) and pair a gz stack with a boot watchdog; the unicast relay is a sticky per-boot race that fails ~half the time with >2 gz processes.
- Never write gcloud flag names or Cloud Run limits from memory. CLI shape and quotas drift; verify against cloud.google.com/sdk/gcloud/reference/run/deploy before committing a real deploy. The robotics flag values below are robium-verified (nav-trial), not guesses; the availability of a given flag in your gcloud version still needs the reference check.
Quick start
1. Build the image and push to Artifact Registry. Two paths (both are real
gcloud surfaces; verify current flags against the reference above):
# One-shot: Cloud Build builds from the Dockerfile and pushes in one call.
gcloud builds submit --tag \
<region>-docker.pkg.dev/<project>/<repo>/<image>:<tag>
# Or deploy straight from source (Cloud Run builds it for you):
gcloud run deploy demo-<app> --source . --region=<region>
(The Artifact Registry repo is created once with gcloud artifacts repositories create <repo> --repository-format=docker --location=<region>.)
2. Deploy the container with the robotics/sim flag set (values verified for a Gazebo Harmonic + Nav2 stack, nav-trial 2026-07-13):
gcloud run deploy demo-<app> \
--image=<region>-docker.pkg.dev/<project>/<repo>/<image>:<tag> \
--region=<region> --port=8765 \
--concurrency=4 --session-affinity \
--min-instances=0 --max-instances=5 --timeout=1800 \
--cpu=8 --memory=8Gi --cpu-boost --no-cpu-throttling \
--execution-environment=gen2 \
--set-env-vars=GZ_RELAY=127.0.0.1,GZ_IP=127.0.0.1,FASTDDS_BUILTIN_TRANSPORTS=UDPv4 \
--allow-unauthenticated --quiet
Why each robotics-specific piece is there is in Usage patterns and Platform
gotchas below. --timeout=1800 caps a single request (and so a WebSocket
session) at 30 min; Cloud Run's request timeout is the session cap for a
long-lived ws, so size it to the demo, not the default.
3. Map a same-site subdomain (e.g. demo.yourdomain.org → the service) if
the page that talks to it lives on your site: the session-affinity cookie is
SameSite-Lax and never flows cross-site to a *.run.app host (see gotchas).
Usage patterns
Choose the billing mode: the central cost decision.
| Mode | Flag | CPU allocated | Use when |
|---|---|---|---|
| Request-based (default) | (none) | Only while a request is open; $0 truly idle | A held connection spans the whole boot (the viewer's ws holds CPU) |
| Instance-based | --no-cpu-throttling |
Whole instance lifetime | Start-before-view flows where the boot has no held connection |
Instance-based cost: Cloud Run retains idle instances up to ~15 min after the
last request, ≈ $0.20–0.40 per session-end at 8 vCPU / 8 GiB; a churning fleet
can reach a few $/hour. --min-instances=0 still guarantees $0 when untouched.
(Verified nav-trial, cloud-run-tuning notes.)
Concurrency & session affinity for per-visitor instances.
--concurrency=1gives per-connection instances with zero code, but only that one connection ever reaches the instance: no room for a status endpoint or a stop button. Right for deep-link-only flows.--concurrency=4 --session-affinitylets a page's status polls and stop calls reach the same instance as its ws, but the affinity cookie (GAESA) is SameSite-Lax and is silently never sent cross-site. Map a same-site subdomain, send fetches withcredentials:'include', and answer exact-origin CORS withAllow-Credentials. Without this, routing is pot luck and 409s plague the status feed. Browser connection pooling pins fetches to one backend, so cookie affinity, not luck, must do the routing.
Long-lived WebSockets (Foxglove bridge, PTY-over-ws). Cloud Run supports
WebSockets, but the request timeout (--timeout, up to 60 min) bounds the
session; a ws is one long request. Set it to the intended session cap. When
probing a boot by hand from a shell, hold the connection (curl -N --http1.1 --max-time 400 with the Upgrade headers): a probe that connects-and-drops
leaves a request-based instance CPU-frozen mid-boot. The bridge subprotocol
(foxglove.sdk.v1 on bridge ≥3.x; h2 breaks the upgrade: use --http1.1) is
the foxglove skill's territory.
Deploy auth from CI / servers (GCP_SA_KEY). A service-account key (JSON,
one env var) activates gcloud non-interactively. Materialize it to a temp file
and point GOOGLE_APPLICATION_CREDENTIALS at it; robium keeps the key in
Doppler (GCP_SA_KEY, SA robium-deployer@robium-prod, deploy-scoped: Cloud
Run + Cloud Build + Artifact Registry + Storage + Service Account User):
f="$(mktemp)"; printf '%s' "$GCP_SA_KEY" > "$f"
gcloud auth activate-service-account --key-file="$f" --quiet
export GOOGLE_APPLICATION_CREDENTIALS="$f"
# ... gcloud builds submit / gcloud run deploy ...
rm -f "$f"
See robium's secrets doc for the Doppler/.env wiring (the environments skill's
secrets guidance points here for the Cloud Run half). In GitHub Actions the same
key feeds google-github-actions/auth + deploy-cloudrun.
Fleet visibility (the "N of budget" count). Query Cloud Monitoring
(run.googleapis.com/container/instance_count, sum of latest points) with the
metadata-server token; grant the runtime SA roles/monitoring.viewer once.
Cache ~30 s (expect ~1 min metric lag). Never fetch it on page load; a bare
status request cold-boots a billable instance per drive-by visitor; fold it into
an endpoint that only runs while a session is live (that endpoint is the
live-demo gateway's).
Platform gotchas
All verified deploying robium's nav-trial demo (Gazebo Harmonic + Nav2), 2026-07-12/13, unless dated otherwise. Sources: robium's cloud-run-tuning notes / nav-trial.
- No UDP multicast → gz-transport needs a unicast relay. Set
GZ_RELAY=127.0.0.1+GZ_IP=127.0.0.1. Even then the relay is a sticky per-boot race with >2 gz processes (SO_REUSEPORT flow hashing): a boot either fully works or never recovers. Symptom of a lost race: every client loopsRequesting list of world names., zero gz output, no odom/scan. Pair with an in-container boot watchdog (no sim data within ~120 s → SIGINT PID 1 → a fresh instance boots on the client's reconnect); this recovers the ~50% of boots that lose the race. - DDS multicast is equally absent.
FASTDDS_BUILTIN_TRANSPORTS=UDPv4silences the default shared-memory transport'sopen_and_lock_fileerror storm; single-container FastDDS then unicasts on loopback fine. - Request-based billing throttles CPU between requests. A background sim
boot with no held connection freezes mid-boot. Use
--no-cpu-throttlingfor start-button flows, or make the boot ride a held ws (see Usage patterns). - A health probe that connects-and-drops throttles the container mid-boot. Under request-based billing the probe is the only open request; when it drops, CPU is deallocated with the sim half-booted. Probes and manual boot checks must hold the socket open for the boot's duration.
- Session-affinity cookies are SameSite-Lax: they never flow from your
site's pages to a
*.run.apphost (cross-site). Map a same-site subdomain to the service and usecredentials:'include'+ exact-origin CORS, or affinity silently does nothing and status/stop calls misroute (409s). - Cold start includes image pull on a fresh node. A ~2.5 GB ROS image adds 30–90 s to the first boot on a node; say so on any user-facing page rather than letting it read as a hang.
- Direct VPC egress: size the subnet for the whole instance pool. If you
attach the service to a VPC, the subnet must hold IPs for
--max-instancescontainers, not one. A/28(16 IPs) failed to deploy at--max-instances=5 --cpu=8withno sufficient IP addresses in the VPC network; a/24fixed it. Widen in place withgcloud compute networks subnets expand-ip-range; no need to recreate the subnet or service. - Public interactive shells need a real threat model, not just
--allow-unauthenticated. The exposures that matter on a browser-reachable container shell are its credentials and its network egress, not container escape. robium's candidate hardening shape, a zero-IAM-role runtime service account plus deny-all VPC egress (egress-lockdown), is deployed but not yet verified end-to-end, so treat it as a direction, not a recipe: until it is proven, don't put a public shell on a container holding any credential you'd mind losing. (Status per live-demo's orchestrator notes; re-check before relying on it.)
Customization
- Different sim/stack: the transport env vars (
GZ_RELAY/GZ_IP,FASTDDS_BUILTIN_TRANSPORTS) and the watchdog apply to any gz/ROS 2 stack; a non-ROS backend (a policy server, a Gradio app) drops the gz/DDS env and the watchdog and keeps only the billing-mode, timeout, concurrency, and affinity choices. Size--cpu/--memoryto the workload (a Gazebo+Nav2 stack wanted 8 vCPU for RTF ≈ 1; measure, don't copy the number). - Different budget/session cap:
--max-instancesis the fleet budget,--timeoutthe per-session cap,--min-instancesa warm floor (leave at 0 for scale-to-zero). State the resulting cost envelope in the app docs. - No VPC: the subnet-sizing gotcha only applies if you attach Direct VPC egress (for egress control or reaching private resources). A plain public demo needs none of it.
- GPU workloads: Cloud Run is CPU-only for robium's purposes; a policy that needs a GPU goes to a GPU host (RunPod, per the environments skill), not here.
References
- Upstream: Cloud Run docs,
gcloud run deployreference (authoritative flag list; re-check before a real deploy), Cloud Build, Artifact Registry, Cloud Run WebSockets guide, gz-transport relay. gcloud CLI shape (gcloud run deploy,--source,gcloud builds submit --tag, deploy-from-source) confirmed 2026-08-01 via context7 fetch of the upstream Cloud Run deploy docs (search-synthesis of Google's docs, not a direct cloud.google.com fetch); re-verify flag availability against the reference link before committing. The robotics flag values and every gotcha above are robium-verified from the nav-trial demo deployment (2026-07-12/13), not from upstream. - Sibling skills:
live-demo(the demo orchestrator, session gateway, mission-control page, and viewer handoff that consume this deploy target; its cloud-run-tuning notes are the origin of the facts here),environments(uv-vs-Docker, local↔remote parity, and GPU-cloud/RunPod, the GPU deploy target alongside this CPU one),foxglove(bridge/subprotocol mechanics for the ws this skill routes through Cloud Run),architect(routes here for the deploy phase). robium's secrets doc (docs/secrets.md) has theGCP_SA_KEYDoppler wiring.
Changelog
1.0.2 (2026-08-03): style pass; removed em dashes throughout (no content changes).
1.0.1 (2026-08-01): anchor IDs added to claim-bearing items (learning-engine Phase 1); no content changes.
1.0.0 (2026-08-01): created (issue #67); consolidates the robotics/headless Cloud Run deploy mechanics and gotchas that were scattered inside live-demo's cloud-run-tuning notes and environments/architect prose: build → Artifact Registry → deploy path, billing-mode cost decision, gz/DDS unicast relay + boot watchdog (no multicast), CPU-on-request throttling and the hold-the-probe rule, SameSite-Lax same-site-subdomain affinity, ws timeout/concurrency, VPC subnet sizing, fleet-count monitoring IAM, deploy auth via
GCP_SA_KEY, and the not-yet-verified egress-lockdown shape for public shells. Cross-referenced from live-demo, environments, and architect's routing table.
Files (robium)
-
SKILL.md 17.4 KB
--- name: cloud-run version: 1.0.2 description: > Deploy headless robotics / sim / demo containers to Google Cloud Run: the build → Artifact Registry → Cloud Run path plus the gotchas that bite sim workloads (no UDP multicast for gz-transport/DDS, CPU allocated only while a request is open, session affinity for per-visitor instances, request timeout / concurrency for long-lived WebSockets, VPC subnet sizing). Use when: 'deploy to Cloud Run', 'gcloud run deploy', 'Cloud Build', 'Artifact Registry', 'host the sim in the cloud', 'Cloud Run WebSocket / Foxglove bridge times out', 'session affinity', '--no-cpu-throttling', 'GZ_RELAY / GZ_IP on Cloud Run', deploy auth from CI with a GCP service-account key. Owns the Cloud Run deploy mechanics that live-demo and environments point at. Not for: the demo orchestrator, session gateway, or mission-control page (live-demo); GPU-cloud/RunPod or uv-vs-Docker choice (environments); general non-robotics gcloud/GCP basics (upstream Google docs). --- # cloud-run The deploy target for robium's headless robotics and demo containers: a sim + bridge (or a policy server) built into an image, pushed to Artifact Registry, and run on Google Cloud Run so a browser can reach it, optionally one private instance per visitor, scale-to-zero when idle. This skill consolidates the Cloud-Run-specific mechanics and the robotics/headless gotchas that were hard-won deploying a Gazebo Harmonic + Nav2 stack (robium's nav-trial demo, 2026-07-12/13). It stays thin on general gcloud/GCP usage (that lives in Google's own docs) and embeds only what a robotics container hits that a plain web service never does. ## When to use this skill - Building and deploying any headless robotics/sim/demo container to Cloud Run: the `gcloud` / Cloud Build → Artifact Registry → `gcloud run deploy` path, and the flag set a sim workload needs (CPU/memory, timeout, concurrency, affinity). - Debugging a robotics container that runs locally but misbehaves on Cloud Run: gz/DDS discovery silent (no multicast), sim boot freezes (CPU throttled between requests), WebSocket drops, affinity cookie ignored, deploy fails on VPC IPs. - Wiring deploy auth from CI/servers with a GCP service-account key. - The trigger phrases in the description: 'deploy to Cloud Run', 'gcloud run deploy', 'Cloud Build', 'Artifact Registry', 'session affinity', '--no-cpu-throttling', 'GZ_RELAY on Cloud Run', 'Cloud Run WebSocket timeout'. - Cross-references: go to the sibling skill instead when the question is: - The demo orchestrator, session gateway (claim/status/shutdown + ws tunnel), mission-control page, or viewer handoff → `live-demo` (it *uses* this skill's deploy mechanics but owns everything demo-shaped above the container). - Whether to use uv vs Docker, local↔remote reproducibility, GPU passthrough, or GPU-cloud/RunPod provisioning → `environments` (Cloud Run is CPU-only and is one deploy target it points at; RunPod is the GPU one). - `foxglove_bridge` mechanics, the `foxglove.sdk.v1` subprotocol, MCAP → the `foxglove` skill (this skill only covers reaching the bridge through Cloud Run's proxy). - The whole-stack decision and where deploy sits → `architect` (routes here). ## Key directives - **Delegation posture: embed + links.** The robotics/headless Cloud Run gotchas (no multicast → unicast relay, CPU-on-request throttling, same-site affinity, VPC subnet sizing, long-ws timeout/concurrency) are embedded here because they exist nowhere upstream in one place; they were derived by deployment. For general gcloud/GCP mechanics (auth, project setup, the full `gcloud run deploy` flag reference, Cloud Build, Artifact Registry repo creation) point upstream to Google's docs; don't restate them from memory. - **Scale-to-zero, and state per-session cost.** <!-- id: scale-to-zero-state-cost --> `--min-instances=0` unless a warm floor is explicitly justified. The billing-mode choice (request-based vs `--no-cpu-throttling`) is a cost decision: state it and its per-session figure in the app/demo docs, and put a GCP budget alert on the project. - **A boot that needs CPU must hold a request while it boots.** <!-- id: boot-needs-held-request --> Cloud Run's default (request-based) billing allocates CPU *only while a request is open*. A robotics container that boots a sim in the background freezes the moment no request is in flight. Either drive the boot with a held connection (the viewer's WebSocket does this) or pick `--no-cpu-throttling`. - **No UDP multicast anywhere on Cloud Run.** <!-- id: no-udp-multicast-cloud-run --> gz-transport and DDS discovery both assume multicast; neither gets it. Force unicast/loopback (see Quick start) and pair a gz stack with a boot watchdog; the unicast relay is a sticky per-boot race that fails ~half the time with >2 gz processes. - **Never write gcloud flag names or Cloud Run limits from memory.** <!-- id: no-gcloud-facts-from-memory --> CLI shape and quotas drift; verify against [cloud.google.com/sdk/gcloud/reference/run/deploy](https://cloud.google.com/sdk/gcloud/reference/run/deploy) before committing a real deploy. The robotics flag *values* below are robium-verified (nav-trial), not guesses; the *availability* of a given flag in your gcloud version still needs the reference check. ## Quick start **1. Build the image and push to Artifact Registry.** <!-- id: build-push-artifact-registry --> Two paths (both are real `gcloud` surfaces; verify current flags against the reference above): ```bash # One-shot: Cloud Build builds from the Dockerfile and pushes in one call. gcloud builds submit --tag \ <region>-docker.pkg.dev/<project>/<repo>/<image>:<tag> # Or deploy straight from source (Cloud Run builds it for you): gcloud run deploy demo-<app> --source . --region=<region> ``` (The Artifact Registry repo is created once with `gcloud artifacts repositories create <repo> --repository-format=docker --location=<region>`.) **2. Deploy the container** <!-- id: deploy-container-robotics-flags --> with the robotics/sim flag set (values verified for a Gazebo Harmonic + Nav2 stack, nav-trial 2026-07-13): ```bash gcloud run deploy demo-<app> \ --image=<region>-docker.pkg.dev/<project>/<repo>/<image>:<tag> \ --region=<region> --port=8765 \ --concurrency=4 --session-affinity \ --min-instances=0 --max-instances=5 --timeout=1800 \ --cpu=8 --memory=8Gi --cpu-boost --no-cpu-throttling \ --execution-environment=gen2 \ --set-env-vars=GZ_RELAY=127.0.0.1,GZ_IP=127.0.0.1,FASTDDS_BUILTIN_TRANSPORTS=UDPv4 \ --allow-unauthenticated --quiet ``` Why each robotics-specific piece is there is in Usage patterns and Platform gotchas below. `--timeout=1800` caps a single request (and so a WebSocket session) at 30 min; Cloud Run's request timeout is the session cap for a long-lived ws, so size it to the demo, not the default. **3. Map a same-site subdomain** <!-- id: map-same-site-subdomain --> (e.g. `demo.yourdomain.org` → the service) if the page that talks to it lives on your site: the session-affinity cookie is SameSite-Lax and never flows cross-site to a `*.run.app` host (see gotchas). ## Usage patterns **Choose the billing mode: the central cost decision.** <!-- id: choose-billing-mode --> | Mode | Flag | CPU allocated | Use when | | --- | --- | --- | --- | | Request-based (default) | *(none)* | Only while a request is open; $0 truly idle | A held connection spans the whole boot (the viewer's ws holds CPU) | | Instance-based | `--no-cpu-throttling` | Whole instance lifetime | Start-before-view flows where the boot has no held connection | Instance-based cost: Cloud Run retains idle instances up to ~15 min after the last request, ≈ $0.20–0.40 per session-end at 8 vCPU / 8 GiB; a churning fleet can reach a few $/hour. `--min-instances=0` still guarantees $0 when untouched. (Verified nav-trial, cloud-run-tuning notes.) **Concurrency & session affinity for per-visitor instances.** - `--concurrency=1` gives per-connection instances with zero code, but *only* <!-- id: concurrency-1-per-connection --> that one connection ever reaches the instance: no room for a status endpoint or a stop button. Right for deep-link-only flows. - `--concurrency=4 --session-affinity` lets a page's status polls and stop calls <!-- id: concurrency-4-affinity-cookie --> reach the same instance as its ws, **but** the affinity cookie (`GAESA`) is SameSite-Lax and is silently never sent cross-site. Map a same-site subdomain, send fetches with `credentials:'include'`, and answer exact-origin CORS with `Allow-Credentials`. Without this, routing is pot luck and 409s plague the status feed. Browser connection pooling pins fetches to one backend, so cookie affinity, not luck, must do the routing. **Long-lived WebSockets (Foxglove bridge, PTY-over-ws).** <!-- id: long-lived-websockets-timeout --> Cloud Run supports WebSockets, but the request timeout (`--timeout`, up to 60 min) bounds the session; a ws is one long request. Set it to the intended session cap. When probing a boot by hand from a shell, *hold the connection* (`curl -N --http1.1 --max-time 400` with the Upgrade headers): a probe that connects-and-drops leaves a request-based instance CPU-frozen mid-boot. The bridge subprotocol (`foxglove.sdk.v1` on bridge ≥3.x; h2 breaks the upgrade: use `--http1.1`) is the `foxglove` skill's territory. **Deploy auth from CI / servers (`GCP_SA_KEY`).** <!-- id: deploy-auth-ci-gcp-sa-key --> A service-account key (JSON, one env var) activates gcloud non-interactively. Materialize it to a temp file and point `GOOGLE_APPLICATION_CREDENTIALS` at it; robium keeps the key in Doppler (`GCP_SA_KEY`, SA `robium-deployer@robium-prod`, deploy-scoped: Cloud Run + Cloud Build + Artifact Registry + Storage + Service Account User): ```bash f="$(mktemp)"; printf '%s' "$GCP_SA_KEY" > "$f" gcloud auth activate-service-account --key-file="$f" --quiet export GOOGLE_APPLICATION_CREDENTIALS="$f" # ... gcloud builds submit / gcloud run deploy ... rm -f "$f" ``` See robium's secrets doc for the Doppler/`.env` wiring (the environments skill's secrets guidance points here for the Cloud Run half). In GitHub Actions the same key feeds `google-github-actions/auth` + `deploy-cloudrun`. **Fleet visibility (the "N of budget" count).** <!-- id: fleet-visibility-monitoring --> Query Cloud Monitoring (`run.googleapis.com/container/instance_count`, sum of latest points) with the metadata-server token; grant the runtime SA `roles/monitoring.viewer` once. Cache ~30 s (expect ~1 min metric lag). Never fetch it on page load; a bare status request cold-boots a billable instance per drive-by visitor; fold it into an endpoint that only runs while a session is live (that endpoint is the `live-demo` gateway's). ## Platform gotchas All verified deploying robium's nav-trial demo (Gazebo Harmonic + Nav2), 2026-07-12/13, unless dated otherwise. Sources: robium's cloud-run-tuning notes / nav-trial. - **No UDP multicast → gz-transport needs a unicast relay.** <!-- id: gz-transport-unicast-relay-boot-watchdog --> Set `GZ_RELAY=127.0.0.1` + `GZ_IP=127.0.0.1`. Even then the relay is a *sticky per-boot race* with >2 gz processes (SO_REUSEPORT flow hashing): a boot either fully works or never recovers. Symptom of a lost race: every client loops `Requesting list of world names.`, zero gz output, no odom/scan. Pair with an in-container **boot watchdog** (no sim data within ~120 s → SIGINT PID 1 → a fresh instance boots on the client's reconnect); this recovers the ~50% of boots that lose the race. - **DDS multicast is equally absent.** <!-- id: dds-multicast-fastdds-transport --> `FASTDDS_BUILTIN_TRANSPORTS=UDPv4` silences the default shared-memory transport's `open_and_lock_file` error storm; single-container FastDDS then unicasts on loopback fine. - **Request-based billing throttles CPU between requests.** A background sim boot with no held connection freezes mid-boot. Use `--no-cpu-throttling` for start-button flows, or make the boot ride a held ws (see Usage patterns). - **A health probe that connects-and-drops throttles the container mid-boot.** <!-- id: health-probe-must-hold-connection --> Under request-based billing the probe *is* the only open request; when it drops, CPU is deallocated with the sim half-booted. Probes and manual boot checks must hold the socket open for the boot's duration. - **Session-affinity cookies are SameSite-Lax**: they never flow from your site's pages to a `*.run.app` host (cross-site). Map a same-site subdomain to the service and use `credentials:'include'` + exact-origin CORS, or affinity silently does nothing and status/stop calls misroute (409s). - **Cold start includes image pull on a fresh node.** <!-- id: cold-start-image-pull --> A ~2.5 GB ROS image adds 30–90 s to the first boot on a node; say so on any user-facing page rather than letting it read as a hang. - **Direct VPC egress: size the subnet for the whole instance pool.** <!-- id: vpc-subnet-sizing --> If you attach the service to a VPC, the subnet must hold IPs for `--max-instances` containers, not one. A `/28` (16 IPs) failed to deploy at `--max-instances=5 --cpu=8` with `no sufficient IP addresses in the VPC network`; a `/24` fixed it. Widen in place with `gcloud compute networks subnets expand-ip-range`; no need to recreate the subnet or service. - **Public interactive shells need a real threat model, not just `--allow-unauthenticated`.** <!-- id: public-shell-threat-model --> The exposures that matter on a browser-reachable container shell are its *credentials* and its *network egress*, not container escape. robium's candidate hardening shape, a zero-IAM-role runtime service account plus deny-all VPC egress (egress-lockdown), is deployed but **not yet verified end-to-end**, so treat it as a direction, not a recipe: until it is proven, don't put a public shell on a container holding any credential you'd mind losing. (Status per live-demo's orchestrator notes; re-check before relying on it.) ## Customization - **Different sim/stack:** the transport env vars (`GZ_RELAY`/`GZ_IP`, `FASTDDS_BUILTIN_TRANSPORTS`) and the watchdog apply to any gz/ROS 2 stack; a non-ROS backend (a policy server, a Gradio app) drops the gz/DDS env and the watchdog and keeps only the billing-mode, timeout, concurrency, and affinity choices. Size `--cpu`/`--memory` to the workload (a Gazebo+Nav2 stack wanted 8 vCPU for RTF ≈ 1; measure, don't copy the number). - **Different budget/session cap:** `--max-instances` is the fleet budget, `--timeout` the per-session cap, `--min-instances` a warm floor (leave at 0 for scale-to-zero). State the resulting cost envelope in the app docs. - **No VPC:** the subnet-sizing gotcha only applies if you attach Direct VPC egress (for egress control or reaching private resources). A plain public demo needs none of it. - **GPU workloads:** Cloud Run is CPU-only for robium's purposes; a policy that needs a GPU goes to a GPU host (RunPod, per the environments skill), not here. ## References - Upstream: [Cloud Run docs](https://cloud.google.com/run/docs), [`gcloud run deploy` reference](https://cloud.google.com/sdk/gcloud/reference/run/deploy) (authoritative flag list; re-check before a real deploy), [Cloud Build](https://cloud.google.com/build/docs), [Artifact Registry](https://cloud.google.com/artifact-registry/docs), [Cloud Run WebSockets guide](https://cloud.google.com/run/docs/triggering/websockets), [gz-transport relay](https://gazebosim.org/api/transport/14/relay.html). gcloud CLI shape (`gcloud run deploy`, `--source`, `gcloud builds submit --tag`, deploy-from-source) confirmed 2026-08-01 via context7 fetch of the upstream Cloud Run deploy docs (search-synthesis of Google's docs, not a direct cloud.google.com fetch); re-verify flag availability against the reference link before committing. The robotics flag *values* and every gotcha above are robium-verified from the nav-trial demo deployment (2026-07-12/13), not from upstream. - Sibling skills: `live-demo` (the demo orchestrator, session gateway, mission-control page, and viewer handoff that consume this deploy target; its cloud-run-tuning notes are the origin of the facts here), `environments` (uv-vs-Docker, local↔remote parity, and GPU-cloud/RunPod, the GPU deploy target alongside this CPU one), `foxglove` (bridge/subprotocol mechanics for the ws this skill routes through Cloud Run), `architect` (routes here for the deploy phase). robium's secrets doc (docs/secrets.md) has the `GCP_SA_KEY` Doppler wiring. ## Changelog <!-- One dated line per battle-tested change, added by skill-author hardening sessions. --> - 1.0.2 (2026-08-03): style pass; removed em dashes throughout (no content changes). - 1.0.1 (2026-08-01): anchor IDs added to claim-bearing items (learning-engine Phase 1); no content changes. - 1.0.0 (2026-08-01): created (issue #67); consolidates the robotics/headless Cloud Run deploy mechanics and gotchas that were scattered inside live-demo's cloud-run-tuning notes and environments/architect prose: build → Artifact Registry → deploy path, billing-mode cost decision, gz/DDS unicast relay + boot watchdog (no multicast), CPU-on-request throttling and the hold-the-probe rule, SameSite-Lax same-site-subdomain affinity, ws timeout/concurrency, VPC subnet sizing, fleet-count monitoring IAM, deploy auth via `GCP_SA_KEY`, and the not-yet-verified egress-lockdown shape for public shells. Cross-referenced from live-demo, environments, and architect's routing table.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.