Deploy a container on Cloud Run without meeting the startup error
Seven steps that remove the documented failure modes before they happen: make the container listen correctly, pick the right resource type, give it its own service account, grant the deployer actAs, reference secrets with the accessor role, deploy, then verify with the three calls that distinguish 429 from 503.
markdownguidev1DE0Q-7AK2stage: deploytrust: officialfamily: computeservice: cloud-runinterface: clicloud run deployPORT0.0.0.0--service-accountrevisionworker poolroles/iam.serviceAccountUserroles/secretmanager.secretAccessormaximum instances
Deploy a container on Cloud Run
Step 1 — make the container satisfy the contract
Three requirements, each of which has its own failure string if you miss it:
- listen on the value of the
PORTenvironment variable, not a hard-coded port; - bind
0.0.0.0, not127.0.0.1; - be a 64-bit Linux image, built on Linux (a Windows-built image with foreign layers fails at import).
Run it locally first. The documented first fix for the startup error is exactly that.
Step 2 — pick the resource type before you deploy anything
| If the work is | Deploy a |
|---|---|
| request-driven — a website, an API, a microservice | service |
| a batch that runs to completion | job (which decomposes into executions and tasks) |
| a background consumer with no public endpoint | worker pool — it does not scale on requests |
| a single addressable long-lived container | instance — no autoscaling |
A background loop deployed as a service will not scale, because it receives no requests. That is the single most common mis-deploy here.
Step 3 — give it its own service account
Not the default. A purpose-made service account per service is what makes every later grant narrow, and it is
what makes an audit log readable. See reference/identity/predefined-roles-and-permissions.md §4.
Step 4 — let the deployer act as it
The deployer needs roles/iam.serviceAccountUser on that service account (and
roles/run.sourceDeveloper for a source deploy). Then pass it explicitly:
--service-account SERVICE_ACCOUNT
Relying on the default Compute Engine service account is how you meet "User does not have permission to access namespace" on a project where somebody has tidied it away.
Step 5 — secrets, with the right role
If the revision references a secret, the runtime service account needs
roles/secretmanager.secretAccessor — the role carrying secretmanager.versions.access.
roles/secretmanager.viewer is not enough and is the usual cause of a 503 at startup.
Step 6 — deploy, and expect a new revision
Every deploy creates an immutable revision carrying the images and the whole configuration. There is no "edit the env var" operation; there is only a new revision. The service's URL is stable across them, so rollback is picking an older revision.
Step 7 — verify, and read 429 and 503 as different problems
| Call | Expected | If not |
|---|---|---|
| request the service URL | 200 | — |
| watch the logs on the first request | the application's own stdout | if the container never logs, it never started: §1 of reference/compute/errors.md |
| load it past the instance ceiling | 429 "The request was aborted because there was no available instance." | that is capacity — raise maximum instances |
| — | 503 "The request failed because the instance could not start successfully." | that is configuration — missing secret, IAM, or network egress |
Never answer a 503 by raising the instance ceiling, and never answer a 429 by changing the configuration. They look identical on a dashboard and they have opposite fixes.
For a migration
This is the cheapest landing place on Google Cloud for an already-containerised service: no cluster, no node
pool, no scaling policy to port. The counterparts — AWS Fargate, App Runner and Lambda, Azure Container Apps
and Container Instances — and what each of them needs that this does not, are in guides/parity/compute.md.