Nov 13, 20268 min read

How to Deploy a chi App to AWS

Run a go-chi API with Postgres and Redis on Fargate, RDS and ElastiCache in your own AWS account, with Encore or by hand.

Illustration for How to Deploy a chi App to AWS

A chi API compiles to one static binary, so it runs almost anywhere on AWS, and that is also why the choice is hard. The binary is the easy part, and the rest of a production setup is a VPC with private subnets, a load balancer, a Fargate service, an RDS database the app can reach and the internet cannot, a Redis cluster, IAM roles, secrets and a pipeline that runs migrations before new tasks start.

This guide deploys a chi link shortener that stores links in Postgres and caches hot redirects in Redis. It starts with Encore, which provisions Fargate, RDS and ElastiCache in your own AWS account from the Go code, then walks through ECS Fargate by hand, ECS Express Mode and Lambda.

Ways to deploy chi to AWS at a glance

EncoreECS Fargate by handECS Express ModeLambda
Infrastructure defined inGo codeTerraform, CDK or consoleECS service settingsSAM, CDK or Terraform
Postgres and RedisRDS and ElastiCache provisioned from codeYou create and wire themYou create and wire themYou create them, plus VPC access
Local environmentencore run with Postgres and RedisDocker ComposeDocker ComposeSAM local or none
DockerfileEncore builds the imageYou write itYou write itZip or image
Runs inYour AWS accountYour AWS accountYour AWS accountYour AWS account

How do you deploy chi to AWS with Encore?

Encore

Encore works alongside chi, so the router and handlers you already have keep running as they are, and the database and cache they need are declared in Go next to them. From that code Encore knows what to build and provision when you push, and it deploys into your own AWS account.

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 links-api --example=hello-world$ cd links-api && go get github.com/go-chi/chi/v5@v5.5.0$ mkdir -p links/migrations

The migration creates the links table:

-- links/migrations/1_create_links.up.sql CREATE TABLE links ( code TEXT PRIMARY KEY, target_url TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );

The service declares the Postgres database and a Redis cluster, and forwards every request to the chi router from a raw endpoint with a fallback route. Redirects read Redis first and fall back to Postgres on a miss:

// links/links.go package links import ( "crypto/rand" "encoding/json" "errors" "math/big" "net/http" "net/url" "time" "encore.dev/storage/cache" "encore.dev/storage/sqldb" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) // RDS for PostgreSQL on AWS. var db = sqldb.NewDatabase("links", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // ElastiCache (Redis) on AWS, for hot redirects. var cluster = cache.NewCluster("links", cache.ClusterConfig{ EvictionPolicy: cache.AllKeysLRU, }) var targets = cache.NewStringKeyspace[string](cluster, cache.KeyspaceConfig{ KeyPattern: "target/:key", DefaultExpiry: cache.ExpireIn(time.Hour), }) //encore:service type Service struct { router chi.Router } func initService() (*Service, error) { r := chi.NewRouter() r.Use(middleware.RequestID, middleware.Recoverer) r.Post("/links", createLink) r.Get("/{code}", redirect) return &Service{router: r}, nil } // Fallback sends every request that no Encore endpoint matches to chi. // //encore:api public raw path=/!fallback func (s *Service) Fallback(w http.ResponseWriter, req *http.Request) { s.router.ServeHTTP(w, req) } const alphabet = "abcdefghijkmnpqrstuvwxyz23456789" func newCode() string { b := make([]byte, 7) for i := range b { n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet)))) b[i] = alphabet[n.Int64()] } return string(b) } func createLink(w http.ResponseWriter, r *http.Request) { var in struct { URL string `json:"url"` } if err := json.NewDecoder(r.Body).Decode(&in); err != nil { http.Error(w, "invalid body", http.StatusBadRequest) return } u, err := url.Parse(in.URL) if err != nil || (u.Scheme != "https" && u.Scheme != "http") || u.Host == "" { http.Error(w, "url must be an absolute http(s) URL", http.StatusBadRequest) return } code := newCode() if _, err := db.Exec(r.Context(), `INSERT INTO links (code, target_url) VALUES ($1, $2)`, code, u.String()); err != nil { http.Error(w, "could not save link", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]string{"code": code}) } func redirect(w http.ResponseWriter, r *http.Request) { ctx, code := r.Context(), chi.URLParam(r, "code") target, err := targets.Get(ctx, code) if errors.Is(err, cache.Miss) { err = db.QueryRow(ctx, `SELECT target_url FROM links WHERE code = $1`, code).Scan(&target) if errors.Is(err, sqldb.ErrNoRows) { http.NotFound(w, r) return } else if err != nil { http.Error(w, "lookup failed", http.StatusInternalServerError) return } _ = targets.Set(ctx, code, target) // a failed cache write only costs a query next time } else if err != nil { http.Error(w, "lookup failed", http.StatusInternalServerError) return } http.Redirect(w, r, target, http.StatusFound) }

encore run starts Postgres and an in-memory Redis locally and applies the migration, so POST /links followed by a GET on the returned code redirects on a fresh clone. chi middleware such as middleware.RequestID keeps working as before, and Encore adds its own tracing around each request.

Deploy to your AWS account

In the Encore dashboard, go to App Settings > Integrations > Connect Cloud and connect your AWS account, which creates an IAM role Encore uses to provision resources, as described in Connect your cloud account. Create an environment on AWS with Fargate as the compute, then push:

~/links-api
$ git add -A && git commit -m "chi link shortener"$ git push encore

Encore builds the binary into an image, pushes it to ECR and provisions a VPC across two availability zones, an Application Load Balancer, the Fargate service in private compute subnets, RDS for PostgreSQL and ElastiCache in private storage subnets, as the AWS infrastructure docs describe. Encore provisions an IAM role for the service, gives it its own database role, and runs migrations through a proxy inside your environment, without exposing the database. Encore builds the Docker image, so you do not maintain a Dockerfile, a task definition or a DATABASE_URL. Staging and preview environments get the same setup.

When the shortener later needs click analytics, a Pub/Sub topic is another declaration in the same package, and the next push provisions SNS and SQS. A coding agent working in this repo changes infrastructure in the same diff as the handler and runs it under encore run before anything is pushed.

Best fit: chi APIs that should run on Fargate with RDS and ElastiCache in your own AWS account, with the same database and cache locally and in every environment.

ECS Fargate by hand

The manual route starts with a Dockerfile, where a multi-stage build keeps the image small and runs the binary as a non-root user on distroless:

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

Then comes the infrastructure, usually in Terraform: a VPC with public and private subnets and NAT gateways, an ECR repository, an ECS cluster, a task definition with an execution role and a task role, a Fargate service, an ALB with a listener, target group and health check path, an RDS instance and an ElastiCache replication group with their subnet and security groups, and the database password and Redis auth token in Secrets Manager, referenced from the task definition. The chi app reads DATABASE_URL and REDIS_ADDR from the environment, and CI builds the image, pushes it, runs migrations from a one-off task inside the VPC and updates the service.

Teams that already run ECS in Terraform get full control from this route, in exchange for a few hundred lines of configuration that each new environment repeats and a local Docker Compose setup that someone has to keep close to it by hand.

Best fit: platform teams with existing ECS modules who want every setting in their own Terraform.

ECS Express Mode

ECS Express Mode creates a Fargate service, load balancer, scaling and HTTPS endpoint from a container image and a few settings, and AWS points App Runner users to it now that App Runner no longer accepts new customers. You still write the Dockerfile and push to ECR, and Express Mode covers the compute side only, so RDS, ElastiCache, the security groups between them and the secrets are yours to create and connect.

Best fit: a single containerized chi service that needs a quick Fargate deployment and already has its data stores.

Lambda

The aws-lambda-go-api-proxy project includes an httpadapter that wraps any http.Handler, so lambda.Start(httpadapter.New(r).ProxyWithContext) runs a chi router behind API Gateway. It costs nothing at idle and scales with traffic, and it changes how the app talks to data: reaching RDS and ElastiCache means running the function in the VPC, connection counts usually call for RDS Proxy, and in-process state such as connection pools and caches resets on every cold start.

Best fit: low or spiky traffic chi APIs with light database use.

How to choose

Pick Encore when you want a chi API with Postgres and Redis running on Fargate, RDS and ElastiCache in your own AWS account, with the infrastructure coming from the Go code and the same setup locally. Build ECS by hand when your team already maintains ECS in Terraform. ECS Express Mode suits a single container that already has its data stores, and Lambda suits spiky traffic with light database use.

Frequently asked questions

How do I deploy a go-chi app to AWS?

Build the chi binary into a container image, push it to ECR and run it on ECS Fargate behind an Application Load Balancer, with RDS and ElastiCache in private subnets if the app needs them.

How do I deploy chi to AWS with Encore?

Mount the chi router in a raw endpoint with //encore:api public raw path=/!fallback, declare the database with sqldb.NewDatabase and the cache with cache.NewCluster, connect your AWS account in the Encore dashboard, create an AWS environment with Fargate and run git push encore. Encore builds the image and provisions Fargate, RDS for PostgreSQL and ElastiCache in your account.

Can I run a chi app on AWS Lambda?

Yes, with aws-lambda-go-api-proxy, whose httpadapter wraps any http.Handler, including a chi router, as a Lambda handler behind API Gateway. It suits low or spiky traffic, but connections to RDS and ElastiCache from Lambda need VPC settings and usually RDS Proxy.

What should a Dockerfile for a chi app look like?

Use a multi-stage build that compiles a static binary with CGO_ENABLED=0 in a golang image, then copy only the binary into a small runtime image such as distroless static with a nonroot user. The final image has no shell or compiler and does not run as root.

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