Oct 28, 20267 min read

How to Deploy a Go API to Google Cloud Run

Two tested ways to run a Go API with Postgres and Pub/Sub on Cloud Run: Encore provisioning Cloud Run, Cloud SQL and Pub/Sub in your GCP project, or a Dockerfile, gcloud and the IAM grants by hand.

Google Cloud Run runs a container per service, scales it with incoming requests and bills for the time instances are serving, which suits a Go API well because a static Go binary starts in milliseconds and fits in a small image. The work around it is what takes time: a Cloud SQL instance and the connection to it, a service account with the right roles, Secret Manager entries, Pub/Sub topics with push subscriptions back to the service, and a local setup that looks enough like all of that to test against.

This guide deploys a small Go notes API with a Postgres table and a Pub/Sub event to Cloud Run in two ways. The Encore version was run locally with encore run (Encore v1.58.8), and the manual version lists every gcloud step, IAM grant and secret it needs. For AWS instead, see How to add a database and deploy a Go API with an AI agent.

Cloud Run deployment options at a glance

EncoreManual (Dockerfile and gcloud)
Container imageBuilt by Encore from the Go codeDockerfile, Cloud Build or local docker build
Postgressqldb.NewDatabase, provisioned as Cloud SQLCloud SQL instance, user, database and connection flags
Pub/Subpubsub.NewTopic and NewSubscription, provisioned in your projectTopics, push subscriptions, an invoker service account
Service accountOne per service, with only the roles it usesCreated and granted by hand
Secretsencore secret set, stored in Secret ManagerSecret Manager entries and --set-secrets
Local developmentencore run starts Postgres and a local brokerDocker Compose or a dev Cloud SQL instance

Encore: Cloud Run, Cloud SQL and Pub/Sub from the Go code

Encore

Encore combines an open source infrastructure SDK for Go and TypeScript with a platform that provisions what the code declares in your own AWS or GCP account. On GCP, each service runs on Cloud Run, databases become Cloud SQL for PostgreSQL and topics become Pub/Sub, and Encore gives each service its own service account, as the GCP infrastructure docs describe.

Encore declares infrastructure in application code, runs it locally and provisions it in your AWS or GCP account
Infrastructure declared in application code, running locally under encore run and provisioned in your own AWS or GCP account on deploy.
~/notes
$ brew install encoredev/tap/encore$ encore app create notes --example=hello-world --lang=go

A service is a Go package with API endpoints. The database is declared next to the handlers that use it, with migrations as numbered SQL files in notes/migrations:

// Service notes is a small Go API deployed to Cloud Run. package notes import ( "context" "errors" "strings" "encore.dev/beta/errs" "encore.dev/storage/sqldb" ) // Cloud SQL on GCP, a local Postgres under `encore run`. var db = sqldb.NewDatabase("notes", sqldb.DatabaseConfig{ Migrations: "./migrations", }) type Note struct { ID int64 `json:"id"` Title string `json:"title"` Body string `json:"body"` } type CreateParams struct { Title string `json:"title"` Body string `json:"body"` } //encore:api public method=POST path=/notes func Create(ctx context.Context, p *CreateParams) (*Note, error) { if strings.TrimSpace(p.Title) == "" { return nil, &errs.Error{Code: errs.InvalidArgument, Message: "title is required"} } n := &Note{Title: p.Title, Body: p.Body} err := db.QueryRow(ctx, `INSERT INTO notes (title, body) VALUES ($1, $2) RETURNING id`, p.Title, p.Body).Scan(&n.ID) if err != nil { return nil, err } if _, err := NoteCreatedTopic.Publish(ctx, &NoteCreated{ID: n.ID}); err != nil { return nil, err } return n, nil } //encore:api public method=GET path=/notes/:id func Get(ctx context.Context, id int64) (*Note, error) { n := &Note{ID: id} err := db.QueryRow(ctx, `SELECT title, body FROM notes WHERE id = $1`, id).Scan(&n.Title, &n.Body) if errors.Is(err, sqldb.ErrNoRows) { return nil, &errs.Error{Code: errs.NotFound, Message: "note not found"} } return n, err }

The migration, notes/migrations/1_create_notes.up.sql, is plain SQL:

CREATE TABLE notes ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, body TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );

Creating a note publishes an event, and a subscription handles it. On Cloud Run, Encore sets up the Pub/Sub topic, the subscription that delivers to the service and a dead-letter topic for messages that keep failing, all from these declarations (Pub/Sub docs):

package notes import ( "context" "encore.dev/pubsub" "encore.dev/rlog" ) type NoteCreated struct { ID int64 `json:"id"` } // Pub/Sub on GCP, an in-process broker under `encore run`. var NoteCreatedTopic = pubsub.NewTopic[*NoteCreated]("note-created", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }) // Indexing is a pure function of the note id, so a redelivery is harmless. var _ = pubsub.NewSubscription(NoteCreatedTopic, "index-note", pubsub.SubscriptionConfig[*NoteCreated]{ Handler: func(ctx context.Context, e *NoteCreated) error { rlog.Info("indexing note", "id", e.ID) return nil }, })

encore run starts Postgres, applies the migration and delivers the event locally:

~/notes
$ encore runYour API is running at: http://127.0.0.1:4000$ curl -X POST localhost:4000/notes -d '{"title":"Cloud Run","body":"deployed"}'{"id": 1, "title": "Cloud Run", "body": "deployed"}$ curl localhost:4000/notes/42{"code": "not_found", "message": "note not found", "details": null}$ curl -X POST localhost:4000/notes -d '{"title":" "}'{"code": "invalid_argument", "message": "title is required", "details": null}INF indexing note id=1 service=notes subscription=index-note topic=note-created

To deploy, connect your GCP project, create an environment that runs on Cloud Run and push. Encore builds the image, creates the Cloud SQL instance and database, the Pub/Sub resources and the Cloud Run service with its service account in your own project, and applies the migrations on deploy. Secrets set with encore secret set --type prod are stored in Secret Manager in your project. The database docs cover connection details and encore db shell.

The Infrastructure page in the Encore dashboard with a database's configuration panel open, showing fields for vCPUs, memory, storage, availability type, max connections and backups.
Each provisioned resource can be reviewed and its configuration edited per environment in the Encore dashboard. A GCP Cloud SQL database is shown here.

For an agent asked to add a "note shared" event, the change is one more pubsub.NewTopic and a subscription in the same package, and the agent can watch it deliver under encore run before the pull request is opened.

Good to know

Encore can also deploy to GKE on GCP, as the GCP infrastructure docs describe, chosen per environment without changing the Go code. Each request gets a trace with its queries and the publish call.

Manual: Dockerfile, gcloud and IAM

Without Encore, the same app needs an image, a database, secrets, a service account and Pub/Sub, wired together with gcloud. The app reads PORT and DATABASE_URL from the environment and uses pgx and the Pub/Sub client library.

A multi-stage Dockerfile builds a static binary and runs it as a non-root user on a distroless base:

FROM golang:1.24 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /out/notes ./cmd/notes FROM gcr.io/distroless/static-debian12:nonroot COPY --from=build /out/notes /notes USER nonroot:nonroot ENTRYPOINT ["/notes"]

Then create each resource and grant the service account what it needs. The database password goes into Secret Manager as a full connection string that uses the Cloud SQL Unix socket:

~/notes
$ gcloud artifacts repositories create apps --repository-format=docker --location=europe-west1$ gcloud builds submit --tag europe-west1-docker.pkg.dev/$PROJECT/apps/notes$ gcloud sql instances create notes-db --database-version=POSTGRES_16 --tier=db-f1-micro --region=europe-west1$ gcloud sql databases create notes --instance=notes-db$ gcloud sql users create notes --instance=notes-db --password="$DB_PASSWORD"$ gcloud iam service-accounts create notes-api$ gcloud projects add-iam-policy-binding $PROJECT \ --member=serviceAccount:notes-api@$PROJECT.iam.gserviceaccount.com --role=roles/cloudsql.client$ printf "postgres://notes:$DB_PASSWORD@/notes?host=/cloudsql/$PROJECT:europe-west1:notes-db" | \ gcloud secrets create notes-database-url --data-file=-$ gcloud secrets add-iam-policy-binding notes-database-url \ --member=serviceAccount:notes-api@$PROJECT.iam.gserviceaccount.com --role=roles/secretmanager.secretAccessor$ gcloud pubsub topics create note-created$ gcloud pubsub topics add-iam-policy-binding note-created \ --member=serviceAccount:notes-api@$PROJECT.iam.gserviceaccount.com --role=roles/pubsub.publisher$ gcloud run deploy notes --image europe-west1-docker.pkg.dev/$PROJECT/apps/notes \ --region=europe-west1 --service-account=notes-api@$PROJECT.iam.gserviceaccount.com \ --add-cloudsql-instances=$PROJECT:europe-west1:notes-db \ --set-secrets=DATABASE_URL=notes-database-url:latest --allow-unauthenticated

The subscriber side needs a push subscription that calls an endpoint on the service, authenticated with a service account that has roles/run.invoker on it, and the handler has to verify the token Pub/Sub sends, as the Pub/Sub push docs describe. Migrations run as a Cloud Run job before each deploy, or from CI through the Cloud SQL Auth Proxy.

Good to know

Cloud Run sets the PORT variable and expects the container to listen on it, per the container runtime contract. Every staging or preview copy repeats the list above with different names, which is why most teams move it into Terraform once there is more than one environment.

How to choose

  • Encore if you want the Go code to define the Cloud SQL database, Pub/Sub topics and service accounts, with the same app running locally under encore run and on Cloud Run in your own project.
  • Manual if you are deploying a single container that already has its database elsewhere, or your team keeps all GCP resources in Terraform.

Cloud Run alternatives compares other ways to run containers, and Best Go backend frameworks covers the Go side.

Frequently asked questions

How do I deploy a Go API to Google Cloud Run?

Build a container image that listens on the port in the PORT environment variable, push it to Artifact Registry and run gcloud run deploy with a dedicated service account. With Encore, you declare the database and Pub/Sub topics in Go with sqldb.NewDatabase and pubsub.NewTopic, connect your GCP project and push, and Encore builds the image and provisions Cloud Run, Cloud SQL and Pub/Sub with a service account for the service.

How does a Go service on Cloud Run connect to Cloud SQL?

Either through the Cloud SQL connection that Cloud Run mounts as a Unix socket under /cloudsql when you pass --add-cloudsql-instances, or with the Cloud SQL Go connector over a private IP. The service account needs the Cloud SQL Client role either way.

Does Cloud Run scale a Go API to zero?

By default, yes. Cloud Run removes idle instances and starts new ones on the next request, which adds a cold start. Go binaries start quickly, and you can set a minimum number of instances to keep one warm.

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