Skip to content

Deploy to Google Cloud Run

Run the blog API on Cloud Run from the Dockerfile of your project, with Postgres on Cloud SQL and secrets in Secret Manager.

Note

These steps follow the Cloud Run docs as of 2026-10-22. They were not run on Cloud Run in CI.

What you need

  • A Google Cloud project with billing, and the gcloud CLI (gcloud auth login, gcloud config set project PROJECT_ID).
  • The blog API from the tutorial, or any Dox app, with the Dockerfile of a new Dox app. See Docker for how that file works.
  • A Cloud SQL for PostgreSQL instance with a database and a user. See step 3.

The port: PORT and APP_PORT

Cloud Run puts the port it sends requests to in the PORT env variable (default 8080, or the --port value). Dox listens on APP_PORT, or on PORT when APP_PORT is not set. So do not set APP_PORT on Cloud Run.

If you do set APP_PORT, pass the same port with --port.

1. Turn on the APIs

gcloud services enable run.googleapis.com cloudbuild.googleapis.com \
  artifactregistry.googleapis.com secretmanager.googleapis.com sqladmin.googleapis.com

2. Save the secrets in Secret Manager

openssl rand -hex 16 | tr -d '\n' | gcloud secrets create blog-app-key --data-file=-
printf '%s' '<the database password>' | gcloud secrets create blog-db-password --data-file=-

Let the Cloud Run service account read them. By default this is the Compute Engine service account, PROJECT_NUMBER-compute@developer.gserviceaccount.com:

for s in blog-app-key blog-db-password; do
  gcloud secrets add-iam-policy-binding "$s" \
    --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
    --role="roles/secretmanager.secretAccessor"
done

3. Reach Cloud SQL

Cloud Run reaches Cloud SQL by a Unix socket in /cloudsql/... or by a private IP over a VPC. Dox can use both. This guide uses the socket, because it needs no VPC:

  1. Find the connection name of the instance: gcloud sql instances describe INSTANCE --format='value(connectionName)'. It looks like PROJECT_ID:europe-west1:INSTANCE.
  2. Deploy with --add-cloudsql-instances <connection name> (step 4). Cloud Run then makes the socket in /cloudsql/<connection name>.
  3. Give the service account the roles/cloudsql.client role.
  4. Set DB_HOST=/cloudsql/<connection name>. A host that starts with / is a Unix socket. See Unix socket.

The same with one url: DATABASE_URL=postgres://blog:<password>@/blog?host=/cloudsql/<connection name>.

For Cloud SQL for MySQL, use the same DB_HOST with DB_CONNECTION=mysql. Leave out DB_PORT. Or use one url: DATABASE_URL=mysql://blog:<password>@/blog?socket=/cloudsql/<connection name>.

To use the private IP instead: give the instance a private IP on your VPC network, deploy with Direct VPC egress (--network default --subnet default --vpc-egress private-ranges-only), and use the private IP as DB_HOST.

Do not use SQLite on Cloud Run. The file system of an instance is in memory, and each instance has its own. The data is lost when the instance stops.

4. Deploy from source

--source . uploads the folder. Cloud Build builds the image with your Dockerfile and saves it in Artifact Registry (repository cloud-run-source-deploy).

gcloud run deploy blog \
  --source . \
  --region europe-west1 \
  --port 3000 \
  --allow-unauthenticated \
  --cpu 1 --memory 512Mi \
  --min-instances 1 \
  --add-cloudsql-instances PROJECT_ID:europe-west1:INSTANCE \
  --set-env-vars APP_ENV=production,APP_TOTAL_ISOLATE=1,DB_CONNECTION=postgres,DB_HOST=/cloudsql/PROJECT_ID:europe-west1:INSTANCE,DB_PORT=5432,DB_NAME=blog,DB_USERNAME=blog \
  --set-secrets APP_KEY=blog-app-key:1,DB_PASSWORD=blog-db-password:1
Flag Why
--port 3000 the port Cloud Run sends requests to. It also sets PORT=3000, and Dox listens on PORT. See the port
--min-instances 1 keeps one instance warm, so the first request does not wait for a cold start. The default is 0. An idle instance still costs money
--add-cloudsql-instances makes the Cloud SQL Unix socket in /cloudsql/...
--set-env-vars config that is not secret
--set-secrets ENV_NAME=SECRET:VERSION. Cloud Run reads the secret when an instance starts. Pin a version number, not latest
APP_TOTAL_ISOLATE=1 one isolate per CPU. Set it to the --cpu value

To build the image yourself instead, push it to Artifact Registry and deploy it with --image:

docker build --platform linux/amd64 -t europe-west1-docker.pkg.dev/PROJECT_ID/blog/api:1 .
docker push europe-west1-docker.pkg.dev/PROJECT_ID/blog/api:1
gcloud run deploy blog --image europe-west1-docker.pkg.dev/PROJECT_ID/blog/api:1 --region europe-west1 --port 3000 ...

Use the same other flags as above.

Check it

gcloud run deploy prints the service URL at the end.

curl https://blog-xxxxxxxx-ew.a.run.app/api/ping
pong
gcloud run services logs read blog --region europe-west1 --limit 20
gcloud beta run services logs tail blog --region europe-west1   # live, needs the log-streaming component

The first log lines after the first start:

Migrating: 2023_07_16_131937700_create_users_table
Migrated: 2023_07_16_131937700_create_users_table
Migrating: 2026_10_22_205345_create_posts_table
Migrated: 2026_10_22_205345_create_posts_table
{"level":"INFO","name":"dox","message":"Server started at http://[::]:3000 with 1 isolate (native)", ...}

If the deploy fails with "the container did not listen on the port", the app listens on a different port than --port. Check APP_PORT and --port.

HTTPS and the client IP

Cloud Run ends TLS and sends plain HTTP to the container. Only the Cloud Run front end can reach the container port. So ['*'] in trustedProxies is safe, and req.ip() and req.scheme() then show the real client:

lib/config/app.dart
AppConfig appConfig = AppConfig(
  // ...
  trustedProxies: <String>['*'],
);

See Deployment: HTTPS.

Update the app

gcloud run deploy blog --source . --region europe-west1
  • Cloud Run keeps the flags of the last revision. You only pass what changes.
  • A new revision starts, runs new migrations, and takes the traffic when it listens on the port. The old one then stops.
  • When many instances start at the same time, each one runs the migrations. Keep migrations safe to run twice, or run one instance during a deploy with a migration.
  • A new secret version: create it with gcloud secrets versions add, then deploy with the new number in --update-secrets APP_KEY=blog-app-key:2.

See also