Production on AWS (Reference Architecture)
This page describes Brutor’s Terraform-defined reference architecture for AWS: ECS Fargate services behind an Application Load Balancer, with Aurora PostgreSQL, ElastiCache Redis and Qdrant. It is a starting point that customers adapt to their own account structure, networking standards and compliance requirements — not a managed service you subscribe to.
If you’re evaluating, start with Docker Compose instead; everything below runs the same images with the same configuration.
Topology
Section titled “Topology” Internet │ ┌──────────▼───────────┐ │ ALB (public subnets) │ TLS terminates here (ACM cert) │ HTTP :80 → HTTPS │ └──────────┬───────────┘ host/path routing │ ┌───────────────┬────────────────┼──────────────────┐ ▼ ▼ ▼ ▼ admin console user portal control plane :5050 core proxy :8100 ECS Fargate, (React) (React) /v1/admin/* /v1/proxy/* private subnets /v1/portal/* /v1/messages ┌─────────────────────────────────────────────────────────────────┐ │ Aurora PostgreSQL ElastiCache Redis Qdrant │ private subnets │ Serverless v2 (shared DB) (counters, cache) (ECS task) │ └─────────────────────────────────────────────────────────────────┘ Secrets Manager (JWT_SECRET, ENCRYPTION_KEY, DB creds, provider keys) ECR (images) · CloudWatch (logs)- VPC
10.0.0.0/16with public subnets for the ALB and private subnets for every ECS service and data store. Default region in the reference iseu-west-1. - TLS terminates at the ALB with an ACM certificate; the port-80 listener only redirects to HTTPS.
- All service images are pulled from a registry (GHCR
ghcr.io/brutor-ai/*or a mirrored ECR), pinned to oneBRUTOR_VERSION.
ALB routing
Section titled “ALB routing”The two planes and two UIs sit behind one load balancer, split by host header and path pattern:
| Match type | Match | Target |
|---|---|---|
| host | app.<your-domain> |
Admin Console |
| host | portal.<your-domain> |
User Portal |
| path | /v1/admin/* |
Control Plane :5050 |
| path | /v1/proxy/*, /v1/portal/*, /v1/messages, /v1/assets/* |
Core Proxy :8100 |
The path rules matter: everything runtime — the OpenAI-compatible proxy, MCP, skills, the Portal API and the native Anthropic /v1/messages endpoint — must reach the core proxy, while only /v1/admin/* reaches the control plane. Target-group health checks hit each service’s public /health endpoint (never put auth in front of it).
Data stores
Section titled “Data stores”| Store | Reference choice | Notes |
|---|---|---|
| PostgreSQL | Aurora Serverless v2 | The one shared database for both planes. The control plane owns the schema and runs Alembic migrations on startup under a PostgreSQL advisory lock, so multiple instances don’t race. |
| Redis | ElastiCache | Rate-limit and quota counters, cache state. Do not run production with REDIS_DISABLED=true — counters must be shared across proxy instances. |
| Qdrant | ECS service (1 vCPU / 2 GB) with persistent storage | Semantic cache + per-tenant KB collections. |
| Media | S3 | Point MEDIA_S3_ENDPOINT at real S3 (path-style addressing is only needed for MinIO). |
| Ollama (optional) | ECS service (2 vCPU / 4 GB) backed by EFS | Only if you want local embeddings instead of a provider. |
Secrets — JWT_SECRET, ENCRYPTION_KEY, database credentials, BRUTOR_LICENSE_KEY, QDRANT_JWT_SIGNING_SECRET, SKILL_RUNNER_JWT_SECRET — live in AWS Secrets Manager and are injected into task definitions, never baked into images. Remember the invariant: core proxy and control plane must receive the same JWT_SECRET and ENCRYPTION_KEY.
Service sizing
Section titled “Service sizing”Reference sizing for a small production deployment; the core proxy is the component that scales with traffic:
| Service | Count × size | Rationale |
|---|---|---|
| Core Proxy | 2 × (2 vCPU / 4 GB) | All runtime traffic; two instances for zero-downtime deploys. |
| Control Plane | 2 × (1 vCPU / 2 GB) | Admin API only — low traffic, redundant for availability. |
| Admin Console | 1 × (256 CPU units / 512 MB) | Lightweight React app server. |
| User Portal | 1 × (256 CPU units / 512 MB) | Lightweight React app server. |
| KB Uploader | 1 × (1 vCPU / 2 GB) | Batch ingestion workload. |
| Qdrant | 1 × (1 vCPU / 2 GB) | Grow with vector volume. |
| Ollama (optional) | 1 × (2 vCPU / 4 GB) + EFS | Only with local embeddings. |
Scale the core proxy horizontally without ceremony: configuration-cache invalidation propagates between instances via PostgreSQL LISTEN/NOTIFY, and shared counters live in Redis — see architecture.
Multi-tenant isolation in production
Section titled “Multi-tenant isolation in production”The isolation model is the same one described in architecture, and it doesn’t relax in the cloud:
- Tenant middleware on both planes; every table scoped by
tenant_id. - Per-tenant Qdrant collections (
brutor_kb_{tenant_id}), accessed only with short-lived (300 s) tenant-scoped JWTs. - Per-tenant Redis key prefixes.
- If tenants can deploy MCP servers, do not use
MCP_DEPLOYMENT_DRIVER=dockerin a shared environment — it drives a Docker socket, which is effectively host root. Useecs(registers task definitions on an operator-owned cluster; requiresAWS_DEFAULT_REGION,BRUTOR_ECS_CLUSTER_NAME,BRUTOR_ECS_SUBNETS,BRUTOR_ECS_SECURITY_GROUPS,BRUTOR_ECS_EXECUTION_ROLE_ARNandBRUTOR_ECS_TASK_ROLE_ARN),k8s, ornoneto refuse container deployments entirely.
Hardening checklist
Section titled “Hardening checklist”Before real users touch the deployment:
- Change every seeded credential —
SYSTEM_ADMIN_PASSWORD,DEFAULT_TENANT_ADMIN_PASSWORD, the database password, and any S3/MinIO defaults carried over. - Generate fresh secrets for
JWT_SECRET,ENCRYPTION_KEY,QDRANT_JWT_SIGNING_SECRETandSKILL_RUNNER_JWT_SECRET, and store them in Secrets Manager. Never reuse trial-bundle values. -
CORS_ALLOW_ALL=falsewithCORS_ORIGINSlisting exactly your admin and portal origins (e.g.https://app.example.com,https://portal.example.com). -
KEY_PROVIDER=aws_kmswith a dedicated KMS key (KMS_KEY_ID), so stored secrets are envelope-encrypted under KMS instead of a static local key. - TLS at the ALB, HTTP redirecting to HTTPS; security groups admit ALB → service ports only, and data stores accept traffic only from service security groups.
-
BRUTOR_DEPLOYMENT_MODEset appropriately (self_hostedfor an operator-run install) andMCP_DEPLOYMENT_DRIVERchosen deliberately (see above). - Keep
/healthpublic (target-group checks need it) but expose nothing except the ALB — no ECS service gets a public IP. - Pin
BRUTOR_VERSIONand follow the upgrade procedure — snapshot the database before major upgrades.
