Nov 11, 20267 min read

How to Deploy an Echo App to Google Cloud Run

Run a Go Echo API on Cloud Run with Cloud SQL for PostgreSQL and a Cloud Storage bucket, with Encore provisioning it in your own GCP project or with Docker, Artifact Registry and gcloud by hand.

Illustration for How to Deploy an Echo App to Google Cloud Run

Echo builds into a single Go binary, which makes it a good fit for Cloud Run, where a container gets HTTPS, autoscaling and scale to zero without a cluster. Getting there still takes a Dockerfile, an Artifact Registry repository, a runtime service account, a Cloud SQL instance with its connector, a Secret Manager entry for the password and a bucket with the right IAM binding, and each of those has to exist again for staging.

This guide deploys an Echo feedback API that stores messages in Postgres and takes screenshots through signed upload URLs into Cloud Storage. It starts with Encore, which provisions Cloud Run, Cloud SQL and the bucket in your own GCP project from the Go code, and then walks through the same deploy with Docker and gcloud.

Ways to deploy Echo to Cloud Run at a glance

EncoreDocker + gcloud by hand
Container imageBuilt by Encore from the Go codeYour Dockerfile, pushed to Artifact Registry
PostgresCloud SQL, declared with sqldb.NewDatabaseCloud SQL instance, user and connector you set up
BucketCloud Storage, declared with objects.NewBucketBucket plus an IAM binding for the service account
Service accountOne per service with access to what it usesOne you create and grant roles to
Local developmentencore run with Postgres and local storageDocker Compose plus a dev bucket or emulator
New environmentCreate it in the dashboard and pushRepeat every command or module per environment

How do you deploy Echo to Cloud Run with Encore?

Encore

Encore works alongside Echo and deploys the app with the infrastructure its Go code declares to your own GCP project. The Echo router stays as it is behind a raw fallback route, and the database and bucket are package-level declarations that Encore turns into Cloud SQL and Cloud Storage.

Set up the app

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

Run in your terminal to get started locally.

~
$ encore app create feedback-api --example=hello-world$ cd feedback-api && go get github.com/labstack/echo/v4@v4.16.3$ mkdir -p feedback/migrations
-- feedback/migrations/1_create_feedback.up.sql CREATE TABLE feedback ( id BIGSERIAL PRIMARY KEY, message TEXT NOT NULL, screenshot_key TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );
// feedback/feedback.go package feedback import ( "fmt" "net/http" "time" "encore.dev/storage/objects" "encore.dev/storage/sqldb" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) // Cloud SQL for PostgreSQL on GCP, Postgres on your laptop with `encore run`. var db = sqldb.NewDatabase("feedback", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // A private Cloud Storage bucket for screenshots. var Screenshots = objects.NewBucket("feedback-screenshots", objects.BucketConfig{}) //encore:service type Service struct { e *echo.Echo } func initService() (*Service, error) { e := echo.New() e.Use(middleware.Recover(), middleware.RequestID()) e.GET("/healthz", func(c echo.Context) error { return c.NoContent(http.StatusOK) }) e.POST("/feedback", create) return &Service{e: e}, nil } //encore:api public raw path=/!fallback func (s *Service) Fallback(w http.ResponseWriter, req *http.Request) { s.e.ServeHTTP(w, req) } func create(c echo.Context) error { var in struct { Message string `json:"message"` HasScreenshot bool `json:"has_screenshot"` } if err := c.Bind(&in); err != nil || in.Message == "" { return echo.NewHTTPError(http.StatusBadRequest, "message is required") } ctx := c.Request().Context() var id int64 if err := db.QueryRow(ctx, `INSERT INTO feedback (message) VALUES ($1) RETURNING id`, in.Message, ).Scan(&id); err != nil { return err } out := map[string]any{"id": id} if in.HasScreenshot { key := fmt.Sprintf("feedback/%d.png", id) // The browser PUTs the image straight to Cloud Storage with this URL. u, err := Screenshots.SignedUploadURL(ctx, key, objects.WithTTL(10*time.Minute)) if err != nil { return err } if _, err := db.Exec(ctx, `UPDATE feedback SET screenshot_key = $1 WHERE id = $2`, key, id, ); err != nil { return err } out["upload_url"] = u.URL } return c.JSON(http.StatusCreated, out) }

encore run starts Postgres locally, applies the migration and stores bucket objects on local disk, so the full flow, including the signed upload URL, works on a fresh checkout with no GCP project. There is no PORT handling in the Echo code, since Encore runs the HTTP server and hands requests to Echo through the fallback endpoint.

Deploy to your GCP project

In the Encore dashboard, open the app's settings and go to Integrations > Connect Cloud to grant Encore's service account access to your GCP organization or a single project, as described in Connect your cloud account. Then create an environment, pick GCP with Cloud Run as the compute, and push:

~/feedback-api
$ git add -A && git commit -m "Echo feedback API"$ git push encore

Encore builds the image and deploys the service to Cloud Run with its own service account, provisions a Cloud SQL for PostgreSQL instance in a private network with daily backups and point-in-time recovery, and creates the Cloud Storage bucket with a unique name, granting the service only the storage operations it performs, as listed in the GCP infrastructure docs. Migrations run as part of the deploy from inside the environment.

When a coding agent later adds a feature such as weekly digests, it declares a cron job or a topic in the same Go package, and the next push creates the matching resources in the GCP project from that same diff.

Best fit: Echo APIs that should run on Cloud Run with Cloud SQL and Cloud Storage in your own GCP project, with the infrastructure declared in the API code and a local setup that matches it.

Docker, Artifact Registry and gcloud by hand

The hand-built route starts with an Echo main that listens on PORT and opens Postgres through the Unix socket Cloud Run mounts when a Cloud SQL instance is attached:

func main() { // With --add-cloudsql-instances, Cloud Run exposes the instance at // /cloudsql/PROJECT:REGION:INSTANCE. dsn := fmt.Sprintf("host=/cloudsql/%s user=%s password=%s dbname=feedback", os.Getenv("INSTANCE_CONNECTION_NAME"), os.Getenv("DB_USER"), os.Getenv("DB_PASS")) pool, err := pgxpool.New(context.Background(), dsn) if err != nil { log.Fatal(err) } defer pool.Close() e := echo.New() e.Use(middleware.Recover()) registerRoutes(e, pool) e.Logger.Fatal(e.Start(":" + os.Getenv("PORT"))) }

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

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

Then the GCP side, with every name and role created up front:

~/feedback-api
$ gcloud artifacts repositories create apps --repository-format=docker --location=europe-west1$ gcloud builds submit --tag europe-west1-docker.pkg.dev/$PROJECT/apps/feedback-api$ gcloud iam service-accounts create feedback-api$ gcloud projects add-iam-policy-binding $PROJECT \ --member=serviceAccount:feedback-api@$PROJECT.iam.gserviceaccount.com --role=roles/cloudsql.client$ gcloud storage buckets add-iam-policy-binding gs://$BUCKET \ --member=serviceAccount:feedback-api@$PROJECT.iam.gserviceaccount.com --role=roles/storage.objectUser$ gcloud run deploy feedback-api --region=europe-west1 \ --image=europe-west1-docker.pkg.dev/$PROJECT/apps/feedback-api \ --service-account=feedback-api@$PROJECT.iam.gserviceaccount.com \ --add-cloudsql-instances=$PROJECT:europe-west1:feedback-db \ --set-secrets=DB_PASS=feedback-db-pass:latest \ --set-env-vars=INSTANCE_CONNECTION_NAME=$PROJECT:europe-west1:feedback-db,DB_USER=app

Before those commands run, the Cloud SQL instance, database, user and the feedback-db-pass secret have to exist, the service account needs roles/secretmanager.secretAccessor on that secret, and signing upload URLs from Cloud Run without a key file needs roles/iam.serviceAccountTokenCreator on the service account itself so it can call signBlob. The Cloud SQL guide for Cloud Run covers the socket and connector options, and the container runtime contract the PORT rules. Migrations need a step that can reach the instance, such as a Cloud Run job running golang-migrate before the service update.

Best fit: teams with an established Terraform or gcloud setup for Cloud Run who want to own every flag, and single services where repeating the setup per environment is acceptable.

How to choose

Pick Encore to keep Echo as it is and get Cloud Run, Cloud SQL and Cloud Storage provisioned in your own GCP project from the API code, with the same database and bucket under encore run. Pick Docker and gcloud when a platform team already owns the Cloud Run, IAM and Cloud SQL setup and wants every role binding explicit.

Frequently asked questions

How do I deploy an Echo app to Cloud Run?

Build a container that listens on the port in the PORT environment variable, push it to Artifact Registry and run gcloud run deploy with the image, a region and a service account. For Postgres, attach the Cloud SQL instance with --add-cloudsql-instances or use the Cloud SQL Go connector, and pass the password with --set-secrets from Secret Manager.

How do I deploy an Echo app to Cloud Run with Encore?

Mount the Echo instance in a raw endpoint with //encore:api public raw path=/!fallback, declare the database with sqldb.NewDatabase and any bucket with objects.NewBucket, connect your GCP account in the Encore dashboard, create an environment on GCP with Cloud Run and run git push encore. Encore builds the image and provisions Cloud Run, Cloud SQL and Cloud Storage in your project.

What port does an Echo app need to listen on in Cloud Run?

The one in the PORT environment variable, which Cloud Run sets to 8080 by default. Start Echo with e.Start(":" + os.Getenv("PORT")) and bind to all interfaces, not to localhost.

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