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

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

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.
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.
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:
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.
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.
encore run and on Cloud Run in your own project.Cloud Run alternatives compares other ways to run containers, and Best Go backend frameworks covers the Go side.
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.
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.
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.