Skip to content

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.

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/16 with public subnets for the ALB and private subnets for every ECS service and data store. Default region in the reference is eu-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 one BRUTOR_VERSION.

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).

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.

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.

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=docker in a shared environment — it drives a Docker socket, which is effectively host root. Use ecs (registers task definitions on an operator-owned cluster; requires AWS_DEFAULT_REGION, BRUTOR_ECS_CLUSTER_NAME, BRUTOR_ECS_SUBNETS, BRUTOR_ECS_SECURITY_GROUPS, BRUTOR_ECS_EXECUTION_ROLE_ARN and BRUTOR_ECS_TASK_ROLE_ARN), k8s, or none to refuse container deployments entirely.

Before real users touch the deployment:

  • Change every seeded credentialSYSTEM_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_SECRET and SKILL_RUNNER_JWT_SECRET, and store them in Secrets Manager. Never reuse trial-bundle values.
  • CORS_ALLOW_ALL=false with CORS_ORIGINS listing exactly your admin and portal origins (e.g. https://app.example.com,https://portal.example.com).
  • KEY_PROVIDER=aws_kms with 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_MODE set appropriately (self_hosted for an operator-run install) and MCP_DEPLOYMENT_DRIVER chosen deliberately (see above).
  • Keep /health public (target-group checks need it) but expose nothing except the ALB — no ECS service gets a public IP.
  • Pin BRUTOR_VERSION and follow the upgrade procedure — snapshot the database before major upgrades.