From localhost:8080 to a public endpoint#

The starting point is a familiar one: an HTTP backend that works on your machine, such as a Go API listening on port 8080. The goal is to expose it on the internet with a domain, several instances, and repeatable deployments from a branch, tag or commit.

Every deployment platform asks for the same inputs: the repository, the language or how to build the image, public or private access, a path prefix, the health check path, CPU and memory, minimum and maximum instances, and when to scale. This guide answers them directly with AWS services: Amazon ECR for the image, Amazon ECS on AWS Fargate to run it, and an Application Load Balancer (ALB) in front.

The code requirement: a health check#

The only code requirement is an endpoint that returns 200 when the process can serve requests. The load balancer polls it and stops sending traffic to instances that fail. This example uses only the Go standard library:

go
// main.go (Go 1.22+: method patterns in ServeMux)
package main

import (
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /api/demo/health-check", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok")) // 200 by default
	})
	mux.HandleFunc("GET /api/demo/hello-world", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("hello world"))
	})
	log.Fatal(http.ListenAndServe(":8080", mux))
}
Minimal service with two routes. "GET /path" patterns require Go 1.22 or later.

Keep the health check cheap. It may check critical dependencies, but if it checks everything, an outage in a secondary service will pull all your instances out of rotation at once.

Documentation: Go · net/http ↗ · Go · Routing enhancements in 1.22 ↗

Where to run it on AWS#

OptionWhat you manageWhen to choose it
ECS on Fargate + ALBTask definition, service, target group, scalingFull control without managing servers; the option this guide details
ECS Express ModeAn image and two IAM roles; AWS creates the service, ALB with TLS and scalingYou want sensible defaults and a fast start
AWS App RunnerImage or source codeOnly if you are already a customer: AWS closed it to new customers and recommends ECS Express Mode

ECS Express Mode creates a Fargate-based ECS service with its own URL, a load balancer with SSL/TLS, auto scaling policies and monitoring, and every resource stays in your account for later tuning. If you need each piece under your control from day one, continue with the manual approach.

Documentation: AWS Fargate ↗ · Amazon ECS · Express Mode ↗ · AWS App Runner · Availability change ↗

Build the image and push it to ECR#

A multi-stage build compiles in an image that has the toolchain and copies only the binary into a minimal final image, which also runs as an unprivileged user:

dockerfile
# Dockerfile
FROM golang:1.25 AS build
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /server .

FROM gcr.io/distroless/static-debian13:nonroot
COPY --from=build /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
The distroless nonroot tag runs the process as an unprivileged user. Build for the architecture declared in the task definition (ARM64 here).

Build the image in CI for the right platform (linux/arm64 or linux/amd64) and push it to an Amazon ECR repository with an immutable tag, such as the version or commit SHA. Avoid deploying latest: you will not know which version is running.

Documentation: Docker · Multi-stage builds ↗ · Docker · Multi-platform builds ↗ · distroless ↗ · Amazon ECR · Pushing an image ↗

The task definition#

The task definition describes how to run the container: image, CPU and memory, port and logs. On Fargate, 256 CPU units (0.25 vCPU) allow 512 MiB, 1 GB or 2 GB of memory; 1024 units equal 1 vCPU.

json
{
  "family": "demo-api",
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "256",
  "memory": "512",
  "runtimePlatform": { "operatingSystemFamily": "LINUX", "cpuArchitecture": "ARM64" },
  "executionRoleArn": "arn:aws:iam::111122223333:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "api",
      "image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/demo-api:1.0.0",
      "essential": true,
      "portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/demo-api",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "api"
        }
      }
    }
  ]
}
Illustrative example. The execution role lets ECS pull the image from ECR and write logs; if your code calls other AWS services, add a task role with only those permissions.

Environment variables go in the task definition; secrets go in as references to Secrets Manager or Parameter Store so they never sit in plain text.

Documentation: Amazon ECS · Task definition parameters ↗ · Amazon ECS · Fargate ↗ · Amazon ECS · awslogs ↗ · Amazon ECS · Execution role ↗ · Amazon ECS · Sensitive data ↗

Service, ALB and health checks#

On Fargate, tasks use the awsvpc network mode, so the ALB target group must use the ip target type, not instance. Set its health check to the /api/demo/health-check path and code 200. By default, the ALB marks a target healthy after 5 consecutive successes and unhealthy after 2 failures.

Put the ALB in public subnets and the tasks in private subnets. If the service should only be reachable by other internal services, use an internal ALB: that is the equivalent of choosing private access on a deployment platform. A path prefix maps to path-based listener rules.

bash
aws ecs create-service \
  --cluster demo \
  --service-name demo-api \
  --task-definition demo-api \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration 'awsvpcConfiguration={subnets=[subnet-private-a,subnet-private-b],securityGroups=[sg-api],assignPublicIp=DISABLED}' \
  --load-balancers targetGroupArn=arn:aws:elasticloadbalancing:us-east-1:111122223333:targetgroup/demo-api/0123456789abcdef,containerName=api,containerPort=8080 \
  --health-check-grace-period-seconds 30 \
  --deployment-configuration 'deploymentCircuitBreaker={enable=true,rollback=true}'
Creates the service with two tasks in private subnets, registered in the target group. Subnet, security group and target group IDs are placeholders.

healthCheckGracePeriodSeconds is how long after a task starts ECS ignores failing health checks; raise it if your service is slow to boot. The deployment circuit breaker detects deployments that never reach a steady state and, with rollback enabled, returns to the last completed deployment.

Documentation: Amazon ECS · ALB and ip target type ↗ · ELB · Health checks ↗ · Amazon ECS · Service parameters ↗ · AWS CLI · ecs create-service ↗ · Amazon ECS · Circuit breaker ↗ · Amazon VPC · Subnets ↗

Scaling and cost#

  • Target tracking: set a target value, for example 60% average service CPU, and Application Auto Scaling adds or removes tasks between your minimum and maximum.
  • Fargate Spot: run interruption-tolerant tasks at a discount; AWS can reclaim them with a two-minute warning. Mix it with regular Fargate capacity to keep a stable baseline.
  • Keep at least two tasks in different Availability Zones so a zone failure does not take the service down.

Documentation: Amazon ECS · Target tracking ↗ · Amazon ECS · Fargate Spot ↗

Verify and troubleshoot#

Once the service is stable, call the same endpoints you used locally, now through the ALB's domain. If targets never turn healthy, check in this order: the container listens on the declared port, the health check path is exact, the task security group accepts traffic from the ALB on that port, the grace period leaves enough boot time, and the CloudWatch logs show no configuration errors.

Sources and scope

Documentation checked on September 25, 2026. Examples and decision criteria are editorial proposals; adapt them to your application's contract and validate them in an authorized test environment.

From design to decision

Compare cloud options

Review pricing, limits, conditions and sources for each option (in Spanish).

Open comparison