Nov 9, 20267 min read

How to Deploy an Echo App to AWS

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.

Ways to deploy Echo to AWS at a glance

EncoreECS Fargate by handElastic BeanstalkLambda
Runs asContainers on Fargate (or EKS)Containers on FargateEC2 instancesFunctions
PostgresRDS provisioned from codeRDS in TerraformRDS you attachRDS, usually with RDS Proxy
Infra code to writeNoneVPC, ALB, ECS, RDS, IAMEnvironment configFunction, API Gateway, IAM
Local PostgresStarted by encore runDocker ComposeDocker ComposeDocker Compose
Deploygit push encoreCI builds, pushes, updates serviceeb deploySAM or CDK deploy

How do you deploy Echo to AWS with Encore?

Encore

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.

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 notes-api --example=hello-world$ cd notes-api && go get github.com/labstack/echo/v4@v4.16.3$ mkdir -p notes/migrations

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.

Deploy to your AWS account

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:

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

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.

ECS Fargate by hand

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

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.

Lambda

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.

How to choose

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.

Frequently asked questions

What is the best way to deploy an Echo app to AWS?

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.

How do I deploy an Echo app to AWS with Encore?

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.

Can I run Echo on AWS Lambda?

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.

What Dockerfile should I use for a Go Echo app?

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.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders