Updated Nov 5, 20266 min read

How to Connect Cloud Run to Cloud SQL for PostgreSQL

Three ways to reach a Cloud SQL Postgres database from a Cloud Run service, and what each one asks you to set up.

Cloud Run and Cloud SQL are the default pairing for a containerized backend on Google Cloud, and most of the setup work is in the connection itself: the service account needs the right role, the instance needs a public or private address, and a Cloud Run service that scales to 40 instances can open 40 connection pools against a database sized for ten.

This guide connects a small TypeScript notes API to Cloud SQL for PostgreSQL. It shows the version where Encore (a tool that works alongside your framework) provisions the database and wires the connection, then the three manual options Google documents: the Unix socket, the Cloud SQL Node.js Connector and private IP.

Cloud Run to Cloud SQL options at a glance

EncoreUnix socketNode.js ConnectorPrivate IP
Who creates the instanceEncore, on deployYouYouYou
IAM setupService account per service, created for youCloud SQL Client roleCloud SQL Client roleNetwork access plus DB user
Network pathPrivate, inside the environment's VPCManaged proxymTLS tunnelYour VPC
MigrationsRun on deployYour pipelineYour pipelineYour pipeline
Local databaseStarted by encore runCloud SQL Auth Proxy or local PostgresSameSame

Each option is covered below, starting with Encore.

Declaring the database and deploying to Cloud Run with Encore

Encore is a tool that works alongside your framework for TypeScript and Go, with a platform that provisions and deploys into your own AWS or GCP account. On GCP, each environment gets its own project with services on Cloud Run and databases on Cloud SQL for PostgreSQL, in a private network, with automated daily backups kept for 7 days and point-in-time recovery.

Application code passes through Encore and infrastructure guardrails to resources provisioned in your own AWS or GCP account.
Encore provisions the infrastructure your application needs in your own AWS or GCP account.

The database is a single declaration in the service that owns it, with SQL migrations next to it:

// notes/notes.ts import { api, APIError } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; // Cloud SQL for PostgreSQL on GCP, RDS on AWS, a local Postgres under encore run. export const db = new SQLDatabase("notes", { migrations: "./migrations" }); interface Note { id: number; body: string; } export const create = api( { expose: true, method: "POST", path: "/notes" }, async ({ body }: { body: string }): Promise<Note> => { const row = await db.queryRow<Note>` INSERT INTO notes (body) VALUES (${body}) RETURNING id, body`; return row!; }, ); export const get = api( { expose: true, method: "GET", path: "/notes/:id" }, async ({ id }: { id: number }): Promise<Note> => { const row = await db.queryRow<Note>`SELECT id, body FROM notes WHERE id = ${id}`; if (!row) throw APIError.notFound("note not found"); return row; }, );

The migration in notes/migrations creates the notes table the queries use:

-- notes/migrations/1_create_notes.up.sql CREATE TABLE notes ( id BIGSERIAL PRIMARY KEY, body TEXT NOT NULL );

The schema lives in notes/migrations/1_create_notes.up.sql, a plain CREATE TABLE statement. encore run starts Postgres locally, applies the migration and serves the API, so the queries are tested against the same engine Cloud SQL runs:

~/notes-app
$ encore run$ curl -s -X POST localhost:4000/notes -d '{"body":"ship the migration"}'{"id":1,"body":"ship the migration"}$ curl -s localhost:4000/notes/1{"id":1,"body":"ship the migration"}$ curl -s localhost:4000/notes/99{"code":"not_found","message":"note not found","details":null,"internal_message":null}

When you push, Encore creates the Cloud SQL instance if the environment does not have one, runs the migrations before the new code starts, and connects the Cloud Run service to it over the private network. You never pick an IP type or grant roles/cloudsql.client by hand, and the instance has no public address. If you already run a Cloud SQL instance, Encore can import it instead of creating a new one.

With the database as part of the application code, a coding agent asked to "store notes in Postgres" writes the migration and the queries in one change and checks them under encore run before anything touches your GCP project.

The Go database docs cover queries and migrations in more depth.

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

Option 1: the built-in Unix socket

Cloud Run can mount a Cloud SQL connection as a Unix socket. You attach the instance at deploy time and point the Postgres driver at /cloudsql/PROJECT:REGION:INSTANCE, as described in Google's Cloud Run connection guide:

gcloud projects add-iam-policy-binding my-project \ --member="serviceAccount:notes-api@my-project.iam.gserviceaccount.com" \ --role="roles/cloudsql.client" gcloud secrets add-iam-policy-binding notes-db-password \ --member="serviceAccount:notes-api@my-project.iam.gserviceaccount.com" \ --role="roles/secretmanager.secretAccessor" gcloud run deploy notes-api \ --image=us-docker.pkg.dev/my-project/apps/notes-api:latest \ --service-account=notes-api@my-project.iam.gserviceaccount.com \ --add-cloudsql-instances=my-project:europe-west1:notes-db \ --set-secrets=DB_PASSWORD=notes-db-password:latest \ --max-instances=10

In the app, pg connects with host: "/cloudsql/my-project:europe-west1:notes-db" and the user, password and database name. By default this path reaches the instance over its public IP, so the instance keeps one, although no authorized networks are opened. The database user, its password secret and the migration step in your pipeline are yours to create.

Option 2: the Cloud SQL Node.js Connector

The Cloud SQL Node.js Connector opens an mTLS tunnel authorized by IAM, which also works from a laptop with your own credentials:

import pg from "pg"; import { Connector } from "@google-cloud/cloud-sql-connector"; const connector = new Connector(); const clientOpts = await connector.getOptions({ instanceConnectionName: process.env.INSTANCE_CONNECTION_NAME!, ipType: "PRIVATE", }); export const pool = new pg.Pool({ ...clientOpts, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: "notes", max: 5, });

The service account needs the Cloud SQL Client role here too. With ipType: "PRIVATE" the Cloud Run service also needs a route into the VPC, which brings you to the third option.

Option 3: private IP over Direct VPC egress

For production, most teams give the instance only a private IP and let Cloud Run reach it through Direct VPC egress. That takes a private services access range peered with the VPC, the instance created with --no-assign-ip, and the Cloud Run service deployed with --network and --subnet flags. The driver then connects to the private address like any Postgres host. It is the setup Google recommends for keeping database traffic off the public internet, and the one with the most pieces to create in Terraform or by hand.

Good to know

Each Cloud Run instance holds its own pool, so the real connection count is the pool size times the instance count. A max: 5 pool on a service capped at 10 instances needs 50 connections, which fits a small Cloud SQL tier. Leaving --max-instances unset is the usual cause of too many clients errors after a traffic spike.

How to choose

Pick Encore when you want Cloud SQL and Cloud Run created together in your own GCP project, migrations applied on every deploy and a local Postgres under encore run, with the same code able to deploy to AWS on RDS later.

Pick the Unix socket for a quick start with an instance you already manage, the Node.js Connector when you want IAM-authorized connections that also work locally, and private IP with Direct VPC egress when the database must have no public address and your team owns the network setup.

Frequently asked questions

How do I connect Cloud Run to Cloud SQL?

Grant the Cloud Run service account the Cloud SQL Client role, then connect through the built-in Unix socket (deploy with --add-cloudsql-instances), the Cloud SQL Node.js Connector, or the instance's private IP over Direct VPC egress. With Encore, you declare new SQLDatabase("notes", { migrations: "./migrations" }) in the service and Encore provisions Cloud SQL, runs the migrations and connects the Cloud Run service on deploy.

Should Cloud Run use public or private IP for Cloud SQL?

Private IP keeps database traffic inside your VPC and lets you turn off the public address entirely, which most production setups want. It needs Direct VPC egress or a Serverless VPC Access connector on the Cloud Run side.

How many connections can a Cloud Run service open to Cloud SQL?

Each container instance opens its own pool, so the total is pool size times the number of instances. Keep the pool small (for example max: 5) and cap the maximum instance count so a traffic spike cannot exhaust the database's max_connections.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders