Run a Go Echo API on AWS with Postgres, from the Encore setup that provisions Fargate and RDS in your account to the ECS, Elastic Beanstalk and Lambda routes by hand.
Echo is a small, fast Go web framework with routing, binding and middleware built in, and running it on a laptop is one go run. Running it on AWS is a different size of job, because a production Echo API needs a container image, a registry, a load balancer, a Postgres database in private subnets, IAM roles for the task and a way to run migrations, and each of those is a separate AWS service.
This guide deploys an Echo notes API with Postgres to AWS, first with Encore, which provisions Fargate, the load balancer and RDS in your own AWS account from the application code, and then with ECS Fargate by hand, Elastic Beanstalk and Lambda.
Encore combines an open source infrastructure SDK with a platform that provisions and deploys into your own AWS or GCP account. Echo keeps its router, binding and middleware, mounted in one raw endpoint with a fallback route, and the database the handlers use is declared in Go, so the same code runs locally and on AWS.
Run in your terminal to get started locally.
The migration creates the table:
-- notes/migrations/1_create_notes.up.sql
CREATE TABLE notes (
id BIGSERIAL PRIMARY KEY,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
The service builds the Echo instance once in initService and forwards every request to it:
// notes/notes.go
package notes
import (
"errors"
"net/http"
"strconv"
"encore.dev/storage/sqldb"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// Postgres locally with `encore run`, Amazon RDS when deployed to AWS.
var db = sqldb.NewDatabase("notes", sqldb.DatabaseConfig{
Migrations: "./migrations",
})
//encore:service
type Service struct {
e *echo.Echo
}
func initService() (*Service, error) {
e := echo.New()
e.Use(middleware.Recover())
e.GET("/healthz", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]bool{"ok": true})
})
e.POST("/notes", createNote)
e.GET("/notes/:id", getNote)
return &Service{e: e}, nil
}
// Fallback hands every request that no Encore endpoint matches to Echo.
//
//encore:api public raw path=/!fallback
func (s *Service) Fallback(w http.ResponseWriter, req *http.Request) {
s.e.ServeHTTP(w, req)
}
type Note struct {
ID int64 `json:"id"`
Body string `json:"body"`
}
func createNote(c echo.Context) error {
var in struct {
Body string `json:"body"`
}
if err := c.Bind(&in); err != nil || in.Body == "" {
return echo.NewHTTPError(http.StatusBadRequest, "body is required")
}
var n Note
err := db.QueryRow(c.Request().Context(),
`INSERT INTO notes (body) VALUES ($1) RETURNING id, body`, in.Body,
).Scan(&n.ID, &n.Body)
if err != nil {
return err
}
return c.JSON(http.StatusCreated, n)
}
func getNote(c echo.Context) error {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid id")
}
var n Note
err = db.QueryRow(c.Request().Context(),
`SELECT id, body FROM notes WHERE id = $1`, id,
).Scan(&n.ID, &n.Body)
if errors.Is(err, sqldb.ErrNoRows) {
return echo.NewHTTPError(http.StatusNotFound, "note not found")
}
if err != nil {
return err
}
return c.JSON(http.StatusOK, n)
}
encore run starts Postgres locally, applies the migration and serves the Echo routes on localhost:4000, so the first POST /notes works on a fresh checkout without a .env file.
In the Encore dashboard, open your app's settings, go to Integrations > Connect Cloud and connect your AWS account, which creates an IAM role that Encore uses to provision resources, as described in Connect your cloud account. Then create an environment, pick AWS and Fargate as the compute, and push:
Encore builds the Go binary into an image, pushes it to ECR, and provisions a VPC across two availability zones, an Application Load Balancer, a Fargate service in private subnets and an RDS for PostgreSQL instance in private storage subnets, as listed in the AWS infrastructure docs. Encore provisions an IAM role for the service and a database role for each service, and migrations run through a proxy inside your environment, so the database is never exposed to the internet. Everything lives in your AWS account.
Adding a bucket, a cron job or Pub/Sub later is one more declaration in Go, and the next push provisions it. An AI coding agent working in this repo changes infrastructure the same way it changes handlers, in code, and tests the result under encore run before the push.
Best fit: Echo APIs that should run on Fargate and RDS in your own AWS account, with infrastructure declared in the API code and the same database setup locally and in preview environments.
The standard AWS route is a container on ECS Fargate. The Echo app reads DATABASE_URL from the environment and listens on a port, and a multi-stage Dockerfile produces a small non-root 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
EXPOSE 8080
ENTRYPOINT ["/api"]
Around that image you create an ECR repository, a VPC with public and private subnets and a NAT gateway, an ECS cluster, a task definition with an execution role that can pull from ECR and read the database secret, a task role for any AWS APIs the app calls, an Application Load Balancer with a target group health check on /healthz, security groups that allow the ALB to reach the tasks and the tasks to reach RDS on port 5432, and an RDS for PostgreSQL instance with a subnet group. Migrations need something inside the VPC, usually a one-off ECS task in the pipeline before the service update. In Terraform this is several hundred lines before the first request is served.
Best fit: teams with an existing Terraform or CDK setup for ECS who want every setting in their own infrastructure code.
Elastic Beanstalk has a Go platform that builds and runs a Go binary on EC2 instances behind a load balancer, with eb deploy doing the upload. It removes most of the ECS wiring for a single app. The limits show up with a second service or a worker, since each one becomes its own environment, and attaching RDS to the environment ties the database lifecycle to it, so production setups create RDS separately and pass the connection string in. Configuration lives in .ebextensions files that are only testable by deploying.
Best fit: a single Echo app on AWS where EC2 instances are acceptable and the team wants the fewest moving parts.
Echo can run on Lambda through aws-lambda-go-api-proxy, whose Echo adapter turns API Gateway or Function URL events into net/http requests. Lambda bills per request and scales to zero, which suits low or spiky traffic. Each execution environment handles one request at a time, so Postgres connections multiply with concurrency and usually need RDS Proxy, cold starts add latency to the first request, and the 15 minute limit rules out long background work in the same function.
Best fit: internal or low-traffic Echo APIs where per-request pricing matters more than steady latency.
Pick Encore when you want the Echo code to stay as it is and get Fargate, a load balancer and RDS provisioned in your own AWS account from the application, with the same Postgres locally. Pick ECS Fargate by hand when an infrastructure team already owns the VPC and ECS setup in Terraform, Elastic Beanstalk for a single app with minimal setup, and Lambda for spiky traffic that can tolerate cold starts.
For an always-on API, run it as a container on ECS Fargate behind an Application Load Balancer, with Postgres on RDS in private subnets. Elastic Beanstalk is simpler for a single app, and Lambda fits spiky, low-traffic APIs if you accept cold starts and an adapter around Echo.
Mount the Echo instance in a raw endpoint with //encore:api public raw path=/!fallback, declare the database with sqldb.NewDatabase, connect your AWS account in the Encore dashboard, create an environment and run git push encore. Encore builds the image and provisions Fargate, an Application Load Balancer and RDS Postgres in your own AWS account.
Yes, with an adapter such as aws-lambda-go-api-proxy, which converts API Gateway or Function URL events into net/http requests for Echo. Each invocation handles one request, connection pools to RDS usually need RDS Proxy, and the 15 minute timeout and cold starts apply.
A multi-stage build: compile a static binary with CGO_ENABLED=0 in a golang image, then copy it into a minimal runtime image such as gcr.io/distroless/static-debian12:nonroot and run it as a non-root user. The final image is a few megabytes and contains no shell or compiler.
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.