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.

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.
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.
Run in your terminal to get started locally.
-- 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.
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:
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.
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:
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.
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.
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.
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.
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.