AWS Deployment (ECS Fargate)
This guide deploys the Gateway on ECS Fargate (containers without servers to manage) behind an Application Load Balancer, using one CloudFormation template.
Because all Gateway state lives in Postgres, the tasks need no storage, no stickiness, and no ordering - this is an ordinary horizontally-scaled web service.
Overview
The template creates:
- An Application Load Balancer, internal by default, with an HTTPS listener.
- An ECS cluster and a Fargate service running N Gateway tasks.
- A target group health-checking
/healthz, with stickiness off. - Security groups so only the load balancer can reach the tasks.
- A CloudWatch log group for container output.
- IAM roles - one to pull the image and read your secrets, one for the task itself.
It deliberately does not create:
| Not created | Why |
|---|---|
| A VPC or subnets | You pass existing ones. |
| A database | The Gateway needs Postgres 14+ that you operate. RDS is the expected choice. |
| The secrets | You create them first, so no credential is ever a CloudFormation parameter in plaintext. |
| A DNS record | Point your hostname at the load balancer, then name that hostname in the pool’s Public base URL setting. |
Architecture
flowchart LR
Clients["MCP clients and agents"]
Britive["Britive tenant"]
Backends["Backend MCP servers"]
subgraph LbSubnets["Load balancer subnets"]
ALB["Application Load Balancer<br/>HTTPS 443"]
end
subgraph TaskSubnets["Private task subnets"]
T1["Gateway task"]
T2["Gateway task"]
end
RDS[("Postgres<br/>(yours)")]
Clients -->|"HTTPS"| ALB
ALB -->|"HTTP 8080"| T1
ALB -->|"HTTP 8080"| T2
T1 --> RDS
T2 --> RDS
T1 -->|"outbound via NAT"| Britive
T1 -->|"outbound"| Backends
Before You Begin
You need:
- An existing VPC with two or more subnets for the load balancer, and private subnets for the tasks. The task subnets need outbound access to your Britive tenant and your backend MCP servers, via NAT or VPC endpoints.
- A Postgres 14+ database the tasks can reach.
- An ACM certificate in the same region, for the HTTPS listener.
- A Britive tenant and pool token.
- Permission to create IAM roles, ECS resources, and load balancers.
Deploy
Download the template
Download mcp-gateway-aws.cfn.yaml - CloudFormation for an ALB plus an ECS Fargate service.
Create the two secrets
The template reads credentials from Secrets Manager rather than taking them as parameters, so they never appear in the stack’s inputs or events.
aws secretsmanager create-secret \
--name britive/mcp-gateway/pool-token \
--secret-string '<your gateway pool token>'
aws secretsmanager create-secret \
--name britive/mcp-gateway/database-url \
--secret-string 'postgresql://gateway:<password>@your-db-host:5432/gateway'Note the two ARNs it returns.
There is no master secret to create or rotate. The key that protects stored tokens and sessions is minted by the platform for the gateway pool and delivered with the pool’s settings, so every task agrees on it and it survives every deployment.
Deploy the stack
aws cloudformation deploy \
--template-file mcp-gateway-aws.cfn.yaml \
--stack-name britive-mcp-gateway \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
Tenant=acme \
GatewayPoolTokenSecretArn=<arn> \
DatabaseUrlSecretArn=<arn> \
VpcId=vpc-xxxxxxxx \
LoadBalancerSubnetIds=subnet-aaa,subnet-bbb \
ServiceSubnetIds=subnet-ccc,subnet-ddd \
CertificateArn=<acm-arn> \
AllowedClientCidr=10.0.0.0/8CAPABILITY_IAM is required because the template creates the two task roles.
Point DNS at the load balancer
aws cloudformation describe-stacks \
--stack-name britive-mcp-gateway \
--query 'Stacks[0].Outputs'Create a DNS record for the hostname clients will use, pointing at
LoadBalancerDnsName.
Set the public base URL on the pool
In the Britive tenant portal, open the gateway pool your token belongs to and set Public base URL to that hostname:
https://mcp-gateway.example.internalIt is a pool setting rather than a stack parameter, so the running tasks pick it up on their next sync - within five minutes by default - with no redeploy.
It must match the hostname clients actually use, because OAuth redirect and resource
URLs are built from it. If DNS and this value disagree, sign-in fails at the callback.
Leave it unset and the tasks fall back to http://localhost:8080 with a warning,
which no client can complete a sign-in against.
Confirm the service is healthy
aws ecs describe-services \
--cluster britive-mcp-gateway \
--services <service-name> \
--query 'services[0].{running:runningCount,desired:desiredCount}'Then, from inside the VPC (or through your VPN if the load balancer is internal):
curl -s https://mcp-gateway.example.internal/healthzVerify
- Both tasks show as healthy in the target group.
GET /healthzreturns 200 through the load balancer.- Signing in at the Gateway’s root URL reaches the admin console.
- Restarting one task leaves you signed in - proof the tasks share state through Postgres.
- Container logs appear in the CloudWatch log group named in the stack outputs.
Things the Template Sets Deliberately
Two settings exist because the defaults would break real usage.
A 300-second load balancer idle timeout. A Britive checkout that requires human approval can outlast the default 60 seconds, and the connection being cut is what fails the tool call. This is an ingress timeout to raise, not a Gateway defect.
Stickiness off, MinimumHealthyPercent 100. Any task can serve any request, so
there is nothing to pin a client to, and a rolling deployment keeps full capacity
while it rotates tasks.
Scaling and Upgrades
Change the replica count:
aws cloudformation deploy --template-file mcp-gateway-aws.cfn.yaml \
--stack-name britive-mcp-gateway \
--capabilities CAPABILITY_IAM \
--parameter-overrides DesiredCount=4 <plus your other parameters>To upgrade the image, force a new deployment - the tasks pull the current image and run migrations on startup:
aws ecs update-service \
--cluster britive-mcp-gateway \
--service <service-name> \
--force-new-deploymentFor a repeatable deployment, pin ImageUri to a specific tag rather than latest,
so what runs is what you chose.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
| Tasks start then stop | Missing or unreadable secret, or an unreachable database | Check the CloudWatch log group; the Gateway names configuration problems explicitly. |
| Task cannot reach the tenant | No NAT or VPC endpoint from the task subnets | Give the private subnets outbound access. |
| Target group never healthy | Health check path or port wrong, or the security group blocks the load balancer | The template sets /healthz on 8080; confirm nothing overrode it. |
ResourceInitializationError reading secrets | Execution role cannot read the ARNs you passed | Confirm the ARNs are in the same account and region. |
| Tasks exit naming the tenant | The tasks cannot reach your tenant, or the pool token was rejected | Fetching settings is fatal at startup by design. Check NAT or VPC endpoints from the service subnets, then the token. |
| OAuth fails at the callback | The pool’s Public base URL doesn’t match DNS | Make them agree in the tenant portal. |
| A settings change hasn’t taken effect | The next sync hasn’t run, or the last one failed | The console’s Settings tab shows what each task is running and flags stale settings. |
| Approval-gated calls time out | Idle timeout lowered below the approval window | Raise it on the load balancer. |