GCP Cloud Run
Deploy your services to Google Cloud Run. The gcp-cloud-run recipe builds your code, builds a Docker image, pushes it to Artifact Registry, deploys a new revision and routes traffic to it.
If the image is already built elsewhere (a vendor image, or one your CI pushes), use GCP Cloud Run (from image) instead. It runs only the deploy and traffic steps and accepts the same cloud_run: options.
Prerequisites
- Google Cloud SDK installed and authenticated
- Docker installed and running
- A GCP project with the Cloud Run and Artifact Registry APIs enabled
- An Artifact Registry Docker repository in each region you deploy to
# Install gcloudbrew install google-cloud-sdk
# Authenticategcloud auth logingcloud auth configure-docker us-central1-docker.pkg.dev
# Create the Artifact Registry repository (once per region)gcloud artifacts repositories create my-repo \ --repository-format=docker \ --location=us-central1 \ --project=my-projectService Configuration
name: my-apitype: gcp-cloud-runproject: my-projectregion: us-central1registry_name: my-repotemplate: go # Dockerfile at _templates/go in the project root
build: language: go cmd: "CGO_ENABLED=0 go build -o ./dist/app ."
cloud_run: min_instances: 0 max_instances: 10 memory: 512Mi cpu: "1" concurrency: 80 timeout_seconds: 300 port: 8080
env_vars: LOG_LEVEL: info
secrets: DATABASE_URL: database-url:latesttype: gcp-cloud-run is what selects this recipe. If you set only provider: gcp and leave out type, Pilum falls back to the first GCP recipe in alphabetical order, which is gcp-artifact-registry-image. That recipe builds and pushes the image but never deploys it. pilum check only warns about this; it does not fail.
Required Fields
| Field | Description |
|---|---|
name | Cloud Run service name. Also the image name, unless image_name is set. |
type | gcp-cloud-run |
project | GCP project ID |
region | GCP region, e.g. us-central1 (or regions: for multi-region) |
registry_name | Artifact Registry repository name. A bare name such as my-repo, not a full host or path. |
template | Dockerfile name under _templates/ in the project root |
pilum check does not report a missing project or region, because the recipe gives them placeholder defaults. Those defaults are only used by pilum init; deploy commands do not apply them. Always set both.
The Dockerfile
Pilum does not look for a Dockerfile in the service directory. The “build docker image” step runs from the project root:
docker build \ -t us-central1-docker.pkg.dev/my-project/my-repo/my-api:v1.0.0 \ -t us-central1-docker.pkg.dev/my-project/my-repo/my-api:latest \ --build-arg SERVICE_NAME=services/my-api \ -f ./_templates/go .template: goresolves to./_templates/go.- The build context is the project root.
SERVICE_NAMEholds the service’s directory path, so one template can serve every service in a monorepo. :latestis tagged locally but only the versioned tag is pushed.pilum initscaffolds a template under_templates/<language>.
If build.cmd is empty, the “build binary” step is skipped and the Dockerfile does all the work.
Optional Fields
Cloud Run Configuration
Each option maps to one gcloud run deploy flag. When a key is unset, Pilum passes no flag, so Cloud Run keeps the service’s current value (or its own default on the first deploy). The defaults listed in the recipe file are what pilum init writes; they are not applied at deploy time.
| Field | Type | Default | Description |
|---|---|---|---|
cloud_run.min_instances | int | - | --min-instances. Set above 0 to avoid cold starts. |
cloud_run.max_instances | int | - | --max-instances |
cloud_run.memory | string | - | --memory, e.g. 512Mi, 1Gi |
cloud_run.cpu | string | - | --cpu, e.g. 1, 2 |
cloud_run.concurrency | int | - | --concurrency: max concurrent requests per instance |
cloud_run.timeout_seconds | int | - | --timeout: request timeout in seconds |
cloud_run.cpu_throttling | bool | - | true passes --cpu-throttling. false passes nothing (see below). |
cloud_run.port | int | - | --port. Sent only when set, except with sidecars, where it is always sent (falls back to 8080). |
cloud_run.use_http2 | bool | false | true passes --use-http2 (end-to-end HTTP/2, cleartext h2c). The container must speak h2c. |
cloud_run.cloudsql_instances | list | - | --add-cloudsql-instances, entries as project:region:instance |
cloud_run.allow_unauthenticated | bool | false | true passes --allow-unauthenticated; otherwise --no-allow-unauthenticated is always sent |
cloud_run.ingress | string | - | --ingress: all, internal-and-cloud-load-balancing or internal |
cloud_run.service_account | string | - | --service-account: the identity the service runs as |
cloud_run.vpc_connector | string | - | --vpc-connector: Serverless VPC Access connector for outbound traffic |
cloud_run.vpc_egress | string | all-traffic | --vpc-egress: all-traffic or private-ranges-only. Only sent with vpc_connector. |
cloud_run.no_traffic | bool | false | --no-traffic, and the “route traffic to latest” step is skipped |
cloud_run.tag | string | - | --tag: gives the new revision its own URL (<tag>---<service>-...run.app) |
cloud_run.env_vars | map | - | Extra environment variables, merged with top-level env_vars |
Image Naming
| Field | Type | Default | Description |
|---|---|---|---|
image_name | string | name | Last path segment of the image, if it should differ from the service name |
version | string | - | Image tag used when --tag is not given. Order: --tag, then version, then latest. |
Things to know
Services are private by default. Every deploy sends --no-allow-unauthenticated unless cloud_run.allow_unauthenticated: true is set. Setting it makes the service public on the next deploy; removing it makes the service private again.
cpu_throttling: false does nothing. Pilum never sends --no-cpu-throttling. To switch a service to always-allocated CPU, run gcloud run services update <name> --no-cpu-throttling once.
Removing a key does not undo most settings. use_http2, port, ingress, service_account, vpc_connector and the scaling options are left untouched when unset, so deleting the line keeps the last value. Change it with gcloud run services update, for example --no-use-http2. cloudsql_instances uses --add-cloudsql-instances, so removing an entry does not detach it.
no_traffic fails on a service’s first deploy. Cloud Run cannot create a service whose only revision takes no traffic. Deploy once without it.
Registry Configuration
Images are pushed to Artifact Registry. registry_name is the bare repository name, and Pilum builds the path from region and project:
project: my-projectregion: us-central1registry_name: my-repobecomes us-central1-docker.pkg.dev/my-project/my-repo/my-api:<tag>.
- If
registry_nameis empty, the project ID is used as the repository name. - If you paste a full host (
...-docker.pkg.dev/...orgcr.io/...) intoregistry_name, Pilum uses it as-is followed by/<image_name>. The bare-name form is the supported convention.
Secrets
secrets is a map of environment variable name to Secret Manager reference:
secrets: DATABASE_URL: database-url:latest API_KEY: projects/my-project/secrets/api-key/versions/3- A value can be
name:versionor the full resource pathprojects/<project>/secrets/<name>/versions/<version>. Full paths are converted toname:version, so the secret must live in the service’s own project. - The service’s runtime service account needs
roles/secretmanager.secretAccessoron each secret.
Pilum always states the full set. With secrets it sends --set-secrets DATABASE_URL=database-url:latest,API_KEY=api-key:3. With no secrets it sends --clear-secrets, which removes every secret from the live service.
Environment Variables
env_vars: LOG_LEVEL: info ENVIRONMENT: production API_TIMEOUT: "30"env_varsmust be a map, and values must be strings: quote numbers and booleans. An unquotedAPI_TIMEOUT: 30is dropped without a warning.cloud_run.env_varsis merged in as well. When a key appears in both, the top-levelenv_varsvalue wins.- As with secrets, Pilum always states the full set:
--set-env-varswhen there are any,--clear-env-varswhen there are none. - Values containing commas are handled with gcloud’s alternate-delimiter syntax.
Sidecars
sidecars adds containers to the same revision. They share the ingress container’s network, so the app can reach a sidecar on localhost.
cloud_run: port: 8080 # the ingress container's port; required in multi-container mode
sidecars: - name: otel-collector image: otel/opentelemetry-collector-contrib:0.98.0 memory: 256Mi cpu: "0.5" env_vars: OTEL_LOG_LEVEL: info secrets: HONEYCOMB_KEY: honeycomb-key:latest args: ["--config", "/etc/otel/config.yaml"]| Field | Type | Description |
|---|---|---|
name | string | Container name (--container) |
image | string | Full image reference. Pilum does not build sidecar images. |
memory | string | --memory for this container |
cpu | string | --cpu for this container |
env_vars | map | This container’s env vars (--set-env-vars / --clear-env-vars) |
secrets | map | This container’s secrets (--set-secrets / --clear-secrets) |
args | list | --args |
command | list | --command (entrypoint override) |
depends_on | list | --depends-on: containers this one waits for at startup |
With sidecars, the ingress container is named after the service. Its --image, --port, --memory, --cpu, env vars and secrets come from the top-level config and cloud_run:.
port and startup_probe are accepted on a sidecar but are not sent to gcloud yet.
Multi-Region Deployment
Use regions: instead of region: to deploy the same service to several regions. Pilum expands it into one instance per region, named my-api (us-central1) and so on:
name: my-apitype: gcp-cloud-runproject: my-projectregistry_name: my-repotemplate: goregions: - us-central1 - europe-west1 - asia-northeast1The image path includes the region, so each region gets its own docker build, its own docker push to <region>-docker.pkg.dev/..., its own gcloud run deploy --region <region> and its own traffic update. The Artifact Registry repository must exist in every listed region.
Deployment Steps
The gcp-cloud-run recipe runs five steps:
- build binary [build] — Runs
build.cmdin the service directory. Skipped whenbuild.cmdis empty. - build docker image [build] —
docker buildfrom the project root with-f ./_templates/<template>. - publish to registry [push] —
docker pushof the versioned tag. - deploy to cloud run [deploy] —
gcloud run deploy. - route traffic to latest [deploy] —
gcloud run services update-traffic <name> --to-latest. Skipped whencloud_run.no_trafficis true.
Step 5 exists because of rollback. A rollback pins traffic to a named revision, and while traffic is pinned, new revisions receive none. Routing to latest makes “deploy” mean “serve this build”. It also resets any traffic split you set by hand.
Before any step runs, Pilum checks that the Docker daemon is up. When build steps will run, it first runs a build cache warming phase; --no-warm skips it.
Example Commands
# Deploy all servicespilum deploy --tag=v1.0.0
# Deploy one servicepilum deploy my-api --tag=v1.0.0
# Build and push only (no deploy)pilum publish --tag=v1.0.0
# Deploy only (the image for this tag must already be in the registry)pilum deploy my-api --only-tags=deploy --tag=v1.0.0
# Preview every command without running anythingpilum dry-run --tag=v1.0.0Always pass --tag (or set version:). Without either, the image is tagged latest.
Generated Deploy Command
For the service configuration above, pilum dry-run --tag=v1.0.0 shows these two deploy-phase commands:
gcloud run deploy my-api \ --region us-central1 \ --platform managed \ --no-allow-unauthenticated \ --min-instances=0 \ --max-instances=10 \ --concurrency=80 \ --timeout=300 \ --project my-project \ --image us-central1-docker.pkg.dev/my-project/my-repo/my-api:v1.0.0 \ --port=8080 \ --memory 512Mi \ --cpu 1 \ --set-env-vars LOG_LEVEL=info \ --set-secrets DATABASE_URL=database-url:latest
gcloud run services update-traffic my-api \ --to-latest \ --region us-central1 \ --project my-projectDry-run prints each command on one line; it is wrapped here for reading. Service-level flags (scaling, ingress, networking, --project) come before --image. Container-level flags (--port, --memory, --cpu, env vars, secrets) come after it, which is the order gcloud needs in multi-container mode.
Rollback, Status and Logs
# Move 100% of traffic back to the previous healthy revisionpilum rollback my-api
# Or to a specific revisionpilum rollback my-api --to my-api-00042-abc
# Show the serving state of each servicepilum status my-api
# Read recent logs (default 50 lines)pilum logs my-api --lines 100pilum rollbackreads the revision list from Cloud Run, not from local history. It picks the newest ready revision created before the one currently serving and runsgcloud run services update-traffic my-api --to-revisions=<revision>=100. Nothing is rebuilt, and that revision’s full configuration comes back with it.- It asks for confirmation; pass
--yesin CI. - The next
pilum deployroutes traffic back to the latest revision. pilum logsrunsgcloud run services logs read.--followhas no effect for Cloud Run.
See Rollback and Status for the full workflow.
Troubleshooting
Service never deploys
pilum dry-run shows only build and publish steps. The service has provider: gcp but no type, so it matched gcp-artifact-registry-image. Add type: gcp-cloud-run.
Secrets disappeared after a deploy
The deploy command contained --clear-secrets because secrets was written as a list, or was removed. Rewrite it as a map (see Secrets) and deploy again.
Authentication errors
ERROR: (gcloud.run.deploy) PERMISSION_DENIEDRe-authenticate. The account also needs permission to deploy Cloud Run services and to act as the runtime service account:
gcloud auth logingcloud auth configure-docker us-central1-docker.pkg.devImage push fails
Docker needs credentials for the region’s Artifact Registry host, and the repository must exist in that region:
gcloud auth configure-docker us-central1-docker.pkg.devgcloud artifacts repositories list --location=us-central1 --project=my-projectService returns 403
The service is private, which is the default. Set cloud_run.allow_unauthenticated: true for a public service, or call it with an identity token.
Service doesn’t start
pilum logs my-apiCheck that the container listens on $PORT (or the cloud_run.port you set).
Next Steps
- GCP Cloud Run (from image) — Deploy a pre-built image
- GCP Cloud Run Jobs — Run-to-completion workloads
- Rollback and Status — Roll back, check status, read logs
- Service Configuration — Full
pilum.yamlreference - CLI Commands — All commands and flags
- Troubleshooting — Common issues