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.
--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. 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=1 gives 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-affinity lets 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 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). 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 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.
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.
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. 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-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. 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,
gcloud run deploy reference
(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 the GCP_SA_KEY
Doppler 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.
1---2name: cloud-run-43description: 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).4---56# cloud-run78The deploy target for robium's headless robotics and demo containers: a sim +9bridge (or a policy server) built into an image, pushed to Artifact Registry,10and run on Google Cloud Run so a browser can reach it, optionally one private11instance per visitor, scale-to-zero when idle. This skill consolidates the12Cloud-Run-specific mechanics and the robotics/headless gotchas that were13hard-won deploying a Gazebo Harmonic + Nav2 stack (robium's nav-trial demo,142026-07-12/13). It stays thin on general gcloud/GCP usage (that lives in15Google's own docs) and embeds only what a robotics container hits that a plain16web service never does.1718## When to use this skill1920- Building and deploying any headless robotics/sim/demo container to Cloud Run:21 the `gcloud` / Cloud Build → Artifact Registry → `gcloud run deploy` path, and22 the flag set a sim workload needs (CPU/memory, timeout, concurrency, affinity).23- Debugging a robotics container that runs locally but misbehaves on Cloud Run:24 gz/DDS discovery silent (no multicast), sim boot freezes (CPU throttled between25 requests), WebSocket drops, affinity cookie ignored, deploy fails on VPC IPs.26- Wiring deploy auth from CI/servers with a GCP service-account key.27- The trigger phrases in the description: 'deploy to Cloud Run', 'gcloud run28 deploy', 'Cloud Build', 'Artifact Registry', 'session affinity',29 '--no-cpu-throttling', 'GZ_RELAY on Cloud Run', 'Cloud Run WebSocket timeout'.30- Cross-references: go to the sibling skill instead when the question is:31 - The demo orchestrator, session gateway (claim/status/shutdown + ws tunnel),32 mission-control page, or viewer handoff → `live-demo` (it *uses* this skill's33 deploy mechanics but owns everything demo-shaped above the container).34 - Whether to use uv vs Docker, local↔remote reproducibility, GPU passthrough,35 or GPU-cloud/RunPod provisioning → `environments` (Cloud Run is CPU-only and36 is one deploy target it points at; RunPod is the GPU one).37 - `foxglove_bridge` mechanics, the `foxglove.sdk.v1` subprotocol, MCAP → the38 `foxglove` skill (this skill only covers reaching the bridge through Cloud39 Run's proxy).40 - The whole-stack decision and where deploy sits → `architect` (routes here).4142## Key directives4344- **Delegation posture: embed + links.** The robotics/headless Cloud Run45 gotchas (no multicast → unicast relay, CPU-on-request throttling, same-site46 affinity, VPC subnet sizing, long-ws timeout/concurrency) are embedded here47 because they exist nowhere upstream in one place; they were derived by48 deployment. For general gcloud/GCP mechanics (auth, project setup, the full49 `gcloud run deploy` flag reference, Cloud Build, Artifact Registry repo50 creation) point upstream to Google's docs; don't restate them from memory.51- **Scale-to-zero, and state per-session cost.** <!-- id: scale-to-zero-state-cost --> `--min-instances=0` unless a52 warm floor is explicitly justified. The billing-mode choice (request-based vs53 `--no-cpu-throttling`) is a cost decision: state it and its per-session54 figure in the app/demo docs, and put a GCP budget alert on the project.55- **A boot that needs CPU must hold a request while it boots.** <!-- id: boot-needs-held-request --> Cloud Run's56 default (request-based) billing allocates CPU *only while a request is open*.57 A robotics container that boots a sim in the background freezes the moment no58 request is in flight. Either drive the boot with a held connection (the59 viewer's WebSocket does this) or pick `--no-cpu-throttling`.60- **No UDP multicast anywhere on Cloud Run.** <!-- id: no-udp-multicast-cloud-run --> gz-transport and DDS discovery61 both assume multicast; neither gets it. Force unicast/loopback (see Quick62 start) and pair a gz stack with a boot watchdog; the unicast relay is a63 sticky per-boot race that fails ~half the time with >2 gz processes.64- **Never write gcloud flag names or Cloud Run limits from memory.** <!-- id: no-gcloud-facts-from-memory --> CLI shape65 and quotas drift; verify against66 [cloud.google.com/sdk/gcloud/reference/run/deploy](https://cloud.google.com/sdk/gcloud/reference/run/deploy)67 before committing a real deploy. The robotics flag *values* below are68 robium-verified (nav-trial), not guesses; the *availability* of a given flag69 in your gcloud version still needs the reference check.7071## Quick start7273**1. Build the image and push to Artifact Registry.** <!-- id: build-push-artifact-registry --> Two paths (both are real74`gcloud` surfaces; verify current flags against the reference above):7576```bash77# One-shot: Cloud Build builds from the Dockerfile and pushes in one call.78gcloud builds submit --tag \79 <region>-docker.pkg.dev/<project>/<repo>/<image>:<tag>8081# Or deploy straight from source (Cloud Run builds it for you):82gcloud run deploy demo-<app> --source . --region=<region>83```8485(The Artifact Registry repo is created once with `gcloud artifacts86repositories create <repo> --repository-format=docker --location=<region>`.)8788**2. Deploy the container** <!-- id: deploy-container-robotics-flags --> with the robotics/sim flag set (values verified for a89Gazebo Harmonic + Nav2 stack, nav-trial 2026-07-13):9091```bash92gcloud run deploy demo-<app> \93 --image=<region>-docker.pkg.dev/<project>/<repo>/<image>:<tag> \94 --region=<region> --port=8765 \95 --concurrency=4 --session-affinity \96 --min-instances=0 --max-instances=5 --timeout=1800 \97 --cpu=8 --memory=8Gi --cpu-boost --no-cpu-throttling \98 --execution-environment=gen2 \99 --set-env-vars=GZ_RELAY=127.0.0.1,GZ_IP=127.0.0.1,FASTDDS_BUILTIN_TRANSPORTS=UDPv4 \100 --allow-unauthenticated --quiet101```102103Why each robotics-specific piece is there is in Usage patterns and Platform104gotchas below. `--timeout=1800` caps a single request (and so a WebSocket105session) at 30 min; Cloud Run's request timeout is the session cap for a106long-lived ws, so size it to the demo, not the default.107108**3. Map a same-site subdomain** <!-- id: map-same-site-subdomain --> (e.g. `demo.yourdomain.org` → the service) if109the page that talks to it lives on your site: the session-affinity cookie is110SameSite-Lax and never flows cross-site to a `*.run.app` host (see gotchas).111112## Usage patterns113114**Choose the billing mode: the central cost decision.** <!-- id: choose-billing-mode -->115116| Mode | Flag | CPU allocated | Use when |117| --- | --- | --- | --- |118| 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) |119| Instance-based | `--no-cpu-throttling` | Whole instance lifetime | Start-before-view flows where the boot has no held connection |120121Instance-based cost: Cloud Run retains idle instances up to ~15 min after the122last request, ≈ $0.20–0.40 per session-end at 8 vCPU / 8 GiB; a churning fleet123can reach a few $/hour. `--min-instances=0` still guarantees $0 when untouched.124(Verified nav-trial, cloud-run-tuning notes.)125126**Concurrency & session affinity for per-visitor instances.**127128- `--concurrency=1` gives per-connection instances with zero code, but *only* <!-- id: concurrency-1-per-connection -->129 that one connection ever reaches the instance: no room for a status endpoint130 or a stop button. Right for deep-link-only flows.131- `--concurrency=4 --session-affinity` lets a page's status polls and stop calls <!-- id: concurrency-4-affinity-cookie -->132 reach the same instance as its ws, **but** the affinity cookie (`GAESA`) is133 SameSite-Lax and is silently never sent cross-site. Map a same-site subdomain,134 send fetches with `credentials:'include'`, and answer exact-origin CORS with135 `Allow-Credentials`. Without this, routing is pot luck and 409s plague the136 status feed. Browser connection pooling pins fetches to one backend, so cookie137 affinity, not luck, must do the routing.138139**Long-lived WebSockets (Foxglove bridge, PTY-over-ws).** <!-- id: long-lived-websockets-timeout --> Cloud Run supports140WebSockets, but the request timeout (`--timeout`, up to 60 min) bounds the141session; a ws is one long request. Set it to the intended session cap. When142probing a boot by hand from a shell, *hold the connection* (`curl -N --http1.1143--max-time 400` with the Upgrade headers): a probe that connects-and-drops144leaves a request-based instance CPU-frozen mid-boot. The bridge subprotocol145(`foxglove.sdk.v1` on bridge ≥3.x; h2 breaks the upgrade: use `--http1.1`) is146the `foxglove` skill's territory.147148**Deploy auth from CI / servers (`GCP_SA_KEY`).** <!-- id: deploy-auth-ci-gcp-sa-key --> A service-account key (JSON,149one env var) activates gcloud non-interactively. Materialize it to a temp file150and point `GOOGLE_APPLICATION_CREDENTIALS` at it; robium keeps the key in151Doppler (`GCP_SA_KEY`, SA `robium-deployer@robium-prod`, deploy-scoped: Cloud152Run + Cloud Build + Artifact Registry + Storage + Service Account User):153154```bash155f="$(mktemp)"; printf '%s' "$GCP_SA_KEY" > "$f"156gcloud auth activate-service-account --key-file="$f" --quiet157export GOOGLE_APPLICATION_CREDENTIALS="$f"158# ... gcloud builds submit / gcloud run deploy ...159rm -f "$f"160```161162See robium's secrets doc for the Doppler/`.env` wiring (the environments skill's163secrets guidance points here for the Cloud Run half). In GitHub Actions the same164key feeds `google-github-actions/auth` + `deploy-cloudrun`.165166**Fleet visibility (the "N of budget" count).** <!-- id: fleet-visibility-monitoring --> Query Cloud Monitoring167(`run.googleapis.com/container/instance_count`, sum of latest points) with the168metadata-server token; grant the runtime SA `roles/monitoring.viewer` once.169Cache ~30 s (expect ~1 min metric lag). Never fetch it on page load; a bare170status request cold-boots a billable instance per drive-by visitor; fold it into171an endpoint that only runs while a session is live (that endpoint is the172`live-demo` gateway's).173174## Platform gotchas175176All verified deploying robium's nav-trial demo (Gazebo Harmonic + Nav2), 2026-07-12/13,177unless dated otherwise. Sources: robium's cloud-run-tuning notes / nav-trial.178179- **No UDP multicast → gz-transport needs a unicast relay.** <!-- id: gz-transport-unicast-relay-boot-watchdog --> Set180 `GZ_RELAY=127.0.0.1` + `GZ_IP=127.0.0.1`. Even then the relay is a *sticky181 per-boot race* with >2 gz processes (SO_REUSEPORT flow hashing): a boot either182 fully works or never recovers. Symptom of a lost race: every client loops183 `Requesting list of world names.`, zero gz output, no odom/scan. Pair with an184 in-container **boot watchdog** (no sim data within ~120 s → SIGINT PID 1 → a185 fresh instance boots on the client's reconnect); this recovers the ~50% of186 boots that lose the race.187- **DDS multicast is equally absent.** <!-- id: dds-multicast-fastdds-transport --> `FASTDDS_BUILTIN_TRANSPORTS=UDPv4`188 silences the default shared-memory transport's `open_and_lock_file` error189 storm; single-container FastDDS then unicasts on loopback fine.190- **Request-based billing throttles CPU between requests.** A background sim191 boot with no held connection freezes mid-boot. Use `--no-cpu-throttling` for192 start-button flows, or make the boot ride a held ws (see Usage patterns).193- **A health probe that connects-and-drops throttles the container mid-boot.** <!-- id: health-probe-must-hold-connection -->194 Under request-based billing the probe *is* the only open request; when it195 drops, CPU is deallocated with the sim half-booted. Probes and manual boot196 checks must hold the socket open for the boot's duration.197- **Session-affinity cookies are SameSite-Lax**: they never flow from your198 site's pages to a `*.run.app` host (cross-site). Map a same-site subdomain to199 the service and use `credentials:'include'` + exact-origin CORS, or affinity200 silently does nothing and status/stop calls misroute (409s).201- **Cold start includes image pull on a fresh node.** <!-- id: cold-start-image-pull --> A ~2.5 GB ROS image adds202 30–90 s to the first boot on a node; say so on any user-facing page rather203 than letting it read as a hang.204- **Direct VPC egress: size the subnet for the whole instance pool.** <!-- id: vpc-subnet-sizing --> If you205 attach the service to a VPC, the subnet must hold IPs for `--max-instances`206 containers, not one. A `/28` (16 IPs) failed to deploy at207 `--max-instances=5 --cpu=8` with `no sufficient IP addresses in the VPC208 network`; a `/24` fixed it. Widen in place with `gcloud compute networks209 subnets expand-ip-range`; no need to recreate the subnet or service.210- **Public interactive shells need a real threat model, not just211 `--allow-unauthenticated`.** <!-- id: public-shell-threat-model --> The exposures that matter on a browser-reachable212 container shell are its *credentials* and its *network egress*, not container213 escape. robium's candidate hardening shape, a zero-IAM-role runtime service214 account plus deny-all VPC egress (egress-lockdown), is deployed but **not yet215 verified end-to-end**, so treat it as a direction, not a recipe: until it is216 proven, don't put a public shell on a container holding any credential you'd217 mind losing. (Status per live-demo's orchestrator notes; re-check before218 relying on it.)219220## Customization221222- **Different sim/stack:** the transport env vars (`GZ_RELAY`/`GZ_IP`,223 `FASTDDS_BUILTIN_TRANSPORTS`) and the watchdog apply to any gz/ROS 2 stack; a224 non-ROS backend (a policy server, a Gradio app) drops the gz/DDS env and the225 watchdog and keeps only the billing-mode, timeout, concurrency, and affinity226 choices. Size `--cpu`/`--memory` to the workload (a Gazebo+Nav2 stack wanted 8227 vCPU for RTF ≈ 1; measure, don't copy the number).228- **Different budget/session cap:** `--max-instances` is the fleet budget,229 `--timeout` the per-session cap, `--min-instances` a warm floor (leave at 0230 for scale-to-zero). State the resulting cost envelope in the app docs.231- **No VPC:** the subnet-sizing gotcha only applies if you attach Direct VPC232 egress (for egress control or reaching private resources). A plain public demo233 needs none of it.234- **GPU workloads:** Cloud Run is CPU-only for robium's purposes; a policy that235 needs a GPU goes to a GPU host (RunPod, per the environments skill), not here.236237## References238239- Upstream: [Cloud Run docs](https://cloud.google.com/run/docs),240 [`gcloud run deploy` reference](https://cloud.google.com/sdk/gcloud/reference/run/deploy)241 (authoritative flag list; re-check before a real deploy),242 [Cloud Build](https://cloud.google.com/build/docs),243 [Artifact Registry](https://cloud.google.com/artifact-registry/docs),244 [Cloud Run WebSockets guide](https://cloud.google.com/run/docs/triggering/websockets),245 [gz-transport relay](https://gazebosim.org/api/transport/14/relay.html).246 gcloud CLI shape (`gcloud run deploy`, `--source`, `gcloud builds submit247 --tag`, deploy-from-source) confirmed 2026-08-01 via context7 fetch of the248 upstream Cloud Run deploy docs (search-synthesis of Google's docs, not a249 direct cloud.google.com fetch); re-verify flag availability against the250 reference link before committing. The robotics flag *values* and every gotcha251 above are robium-verified from the nav-trial demo deployment (2026-07-12/13),252 not from upstream.253- Sibling skills: `live-demo` (the demo orchestrator, session gateway,254 mission-control page, and viewer handoff that consume this deploy target;255 its cloud-run-tuning notes are the origin of the facts here), `environments`256 (uv-vs-Docker, local↔remote parity, and GPU-cloud/RunPod, the GPU deploy257 target alongside this CPU one), `foxglove` (bridge/subprotocol mechanics for258 the ws this skill routes through Cloud Run), `architect` (routes here for the259 deploy phase). robium's secrets doc (docs/secrets.md) has the `GCP_SA_KEY`260 Doppler wiring.261262## Changelog263264<!-- One dated line per battle-tested change, added by skill-author hardening sessions. -->265266- 1.0.2 (2026-08-03): style pass; removed em dashes throughout (no content changes).267268- 1.0.1 (2026-08-01): anchor IDs added to claim-bearing items (learning-engine Phase 1); no content changes.269270- 1.0.0 (2026-08-01): created (issue #67); consolidates the robotics/headless271 Cloud Run deploy mechanics and gotchas that were scattered inside live-demo's272 cloud-run-tuning notes and environments/architect prose: build → Artifact273 Registry → deploy path, billing-mode cost decision, gz/DDS unicast relay +274 boot watchdog (no multicast), CPU-on-request throttling and the hold-the-probe275 rule, SameSite-Lax same-site-subdomain affinity, ws timeout/concurrency, VPC276 subnet sizing, fleet-count monitoring IAM, deploy auth via `GCP_SA_KEY`, and277 the not-yet-verified egress-lockdown shape for public shells. Cross-referenced278 from live-demo, environments, and architect's routing table.