Skip to content

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

  1. Google Cloud SDK installed and authenticated
  2. Docker installed and running
  3. A GCP project with the Cloud Run and Artifact Registry APIs enabled
  4. An Artifact Registry Docker repository in each region you deploy to
Terminal window
# Install gcloud
brew install google-cloud-sdk
# Authenticate
gcloud auth login
gcloud 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-project

Service Configuration

name: my-api
type: gcp-cloud-run
project: my-project
region: us-central1
registry_name: my-repo
template: 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:latest

type: 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

FieldDescription
nameCloud Run service name. Also the image name, unless image_name is set.
typegcp-cloud-run
projectGCP project ID
regionGCP region, e.g. us-central1 (or regions: for multi-region)
registry_nameArtifact Registry repository name. A bare name such as my-repo, not a full host or path.
templateDockerfile 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:

Terminal window
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: go resolves to ./_templates/go.
  • The build context is the project root. SERVICE_NAME holds the service’s directory path, so one template can serve every service in a monorepo.
  • :latest is tagged locally but only the versioned tag is pushed.
  • pilum init scaffolds 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.

FieldTypeDefaultDescription
cloud_run.min_instancesint---min-instances. Set above 0 to avoid cold starts.
cloud_run.max_instancesint---max-instances
cloud_run.memorystring---memory, e.g. 512Mi, 1Gi
cloud_run.cpustring---cpu, e.g. 1, 2
cloud_run.concurrencyint---concurrency: max concurrent requests per instance
cloud_run.timeout_secondsint---timeout: request timeout in seconds
cloud_run.cpu_throttlingbool-true passes --cpu-throttling. false passes nothing (see below).
cloud_run.portint---port. Sent only when set, except with sidecars, where it is always sent (falls back to 8080).
cloud_run.use_http2boolfalsetrue passes --use-http2 (end-to-end HTTP/2, cleartext h2c). The container must speak h2c.
cloud_run.cloudsql_instanceslist---add-cloudsql-instances, entries as project:region:instance
cloud_run.allow_unauthenticatedboolfalsetrue passes --allow-unauthenticated; otherwise --no-allow-unauthenticated is always sent
cloud_run.ingressstring---ingress: all, internal-and-cloud-load-balancing or internal
cloud_run.service_accountstring---service-account: the identity the service runs as
cloud_run.vpc_connectorstring---vpc-connector: Serverless VPC Access connector for outbound traffic
cloud_run.vpc_egressstringall-traffic--vpc-egress: all-traffic or private-ranges-only. Only sent with vpc_connector.
cloud_run.no_trafficboolfalse--no-traffic, and the “route traffic to latest” step is skipped
cloud_run.tagstring---tag: gives the new revision its own URL (<tag>---<service>-...run.app)
cloud_run.env_varsmap-Extra environment variables, merged with top-level env_vars

Image Naming

FieldTypeDefaultDescription
image_namestringnameLast path segment of the image, if it should differ from the service name
versionstring-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-project
region: us-central1
registry_name: my-repo

becomes us-central1-docker.pkg.dev/my-project/my-repo/my-api:<tag>.

  • If registry_name is empty, the project ID is used as the repository name.
  • If you paste a full host (...-docker.pkg.dev/... or gcr.io/...) into registry_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:version or the full resource path projects/<project>/secrets/<name>/versions/<version>. Full paths are converted to name:version, so the secret must live in the service’s own project.
  • The service’s runtime service account needs roles/secretmanager.secretAccessor on 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_vars must be a map, and values must be strings: quote numbers and booleans. An unquoted API_TIMEOUT: 30 is dropped without a warning.
  • cloud_run.env_vars is merged in as well. When a key appears in both, the top-level env_vars value wins.
  • As with secrets, Pilum always states the full set: --set-env-vars when there are any, --clear-env-vars when 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"]
FieldTypeDescription
namestringContainer name (--container)
imagestringFull image reference. Pilum does not build sidecar images.
memorystring--memory for this container
cpustring--cpu for this container
env_varsmapThis container’s env vars (--set-env-vars / --clear-env-vars)
secretsmapThis container’s secrets (--set-secrets / --clear-secrets)
argslist--args
commandlist--command (entrypoint override)
depends_onlist--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-api
type: gcp-cloud-run
project: my-project
registry_name: my-repo
template: go
regions:
- us-central1
- europe-west1
- asia-northeast1

The 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:

  1. build binary [build] — Runs build.cmd in the service directory. Skipped when build.cmd is empty.
  2. build docker image [build] — docker build from the project root with -f ./_templates/<template>.
  3. publish to registry [push] — docker push of the versioned tag.
  4. deploy to cloud run [deploy] — gcloud run deploy.
  5. route traffic to latest [deploy] — gcloud run services update-traffic <name> --to-latest. Skipped when cloud_run.no_traffic is 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

Terminal window
# Deploy all services
pilum deploy --tag=v1.0.0
# Deploy one service
pilum 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 anything
pilum dry-run --tag=v1.0.0

Always 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:

Terminal window
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-project

Dry-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

Terminal window
# Move 100% of traffic back to the previous healthy revision
pilum rollback my-api
# Or to a specific revision
pilum rollback my-api --to my-api-00042-abc
# Show the serving state of each service
pilum status my-api
# Read recent logs (default 50 lines)
pilum logs my-api --lines 100
  • pilum rollback reads the revision list from Cloud Run, not from local history. It picks the newest ready revision created before the one currently serving and runs gcloud 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 --yes in CI.
  • The next pilum deploy routes traffic back to the latest revision.
  • pilum logs runs gcloud run services logs read. --follow has 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_DENIED

Re-authenticate. The account also needs permission to deploy Cloud Run services and to act as the runtime service account:

Terminal window
gcloud auth login
gcloud auth configure-docker us-central1-docker.pkg.dev

Image push fails

Docker needs credentials for the region’s Artifact Registry host, and the repository must exist in that region:

Terminal window
gcloud auth configure-docker us-central1-docker.pkg.dev
gcloud artifacts repositories list --location=us-central1 --project=my-project

Service 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

Terminal window
pilum logs my-api

Check that the container listens on $PORT (or the cloud_run.port you set).

Next Steps