Amazon Web Services (AWS)
This guide deploys Contract Lucidity on AWS using managed services for high availability, auto-scaling, and minimal operational overhead.
Architecture
Prerequisites
- AWS account with administrative permissions
- AWS CLI v2 installed and configured (
aws configure) - Docker installed locally (for building images)
- A registered domain name
Step 1: VPC and Networking
Create a VPC with public and private subnets across two Availability Zones.
# Create VPC
aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=cl-vpc}]' \
--query 'Vpc.VpcId' --output text
# Enable DNS hostnames
aws ec2 modify-vpc-attribute --vpc-id <vpc-id> --enable-dns-hostnames
Create subnets:
| Subnet | CIDR | AZ | Purpose |
|---|---|---|---|
cl-public-1 | 10.0.1.0/24 | us-east-1a | ALB |
cl-public-2 | 10.0.2.0/24 | us-east-1b | ALB |
cl-private-1 | 10.0.10.0/24 | us-east-1a | ECS tasks |
cl-private-2 | 10.0.11.0/24 | us-east-1b | ECS tasks |
cl-data-1 | 10.0.20.0/24 | us-east-1a | RDS, ElastiCache |
cl-data-2 | 10.0.21.0/24 | us-east-1b | RDS, ElastiCache |
Create an Internet Gateway, NAT Gateway, and route tables so:
- Public subnets route to the Internet Gateway
- Private subnets route to the NAT Gateway (for pulling container images and accessing AWS APIs)
- Data subnets have no internet access
Security Groups
| Security Group | Inbound Rules | Purpose |
|---|---|---|
cl-alb-sg | 80, 443 from 0.0.0.0/0 | Load balancer |
cl-ecs-sg | 3000, 8000 from cl-alb-sg | ECS tasks |
cl-rds-sg | 5432 from cl-ecs-sg | PostgreSQL |
cl-redis-sg | 6379 from cl-ecs-sg | Redis |
cl-efs-sg | 2049 from cl-ecs-sg | EFS (if using EFS) |
Step 2: RDS PostgreSQL with pgvector
# Create DB subnet group
aws rds create-db-subnet-group \
--db-subnet-group-name cl-db-subnets \
--db-subnet-group-description "CL data subnets" \
--subnet-ids <cl-data-1-id> <cl-data-2-id>
# Create the RDS instance
aws rds create-db-instance \
--db-instance-identifier cl-postgres \
--db-instance-class db.t3.medium \
--engine postgres \
--engine-version 16.5 \
--master-username cl_user \
--master-user-password '<strong-password>' \
--allocated-storage 50 \
--max-allocated-storage 200 \
--storage-type gp3 \
--db-name contract_lucidity \
--vpc-security-group-ids <cl-rds-sg-id> \
--db-subnet-group-name cl-db-subnets \
--multi-az \
--backup-retention-period 7 \
--storage-encrypted \
--no-publicly-accessible
After the instance is available, enable pgvector:
# Connect via psql (through a bastion or CloudShell in the VPC)
psql -h <rds-endpoint> -U cl_user -d contract_lucidity
# Inside psql
CREATE EXTENSION IF NOT EXISTS vector;
Amazon RDS for PostgreSQL supports pgvector natively (version 0.8.0+ as of March 2026). No custom images or manual compilation required. The extension is available on PostgreSQL 16.5+.
Step 3: ElastiCache Redis
# Create cache subnet group
aws elasticache create-cache-subnet-group \
--cache-subnet-group-name cl-redis-subnets \
--cache-subnet-group-description "CL data subnets" \
--subnet-ids <cl-data-1-id> <cl-data-2-id>
# Create Redis cluster
aws elasticache create-replication-group \
--replication-group-id cl-redis \
--replication-group-description "Contract Lucidity Redis" \
--engine redis \
--engine-version 7.1 \
--cache-node-type cache.t3.small \
--num-cache-clusters 2 \
--cache-subnet-group-name cl-redis-subnets \
--security-group-ids <cl-redis-sg-id> \
--at-rest-encryption-enabled \
--transit-encryption-enabled \
--automatic-failover-enabled
AWS now recommends Valkey (a Redis-compatible fork) for new deployments, priced 20% lower than Redis OSS on ElastiCache. Valkey is fully compatible with Contract Lucidity's Redis usage. To use Valkey, replace --engine redis with --engine valkey.
Step 4: Document Storage (EFS)
EFS provides a shared POSIX filesystem that multiple Fargate tasks can mount simultaneously.
# Create EFS filesystem
aws efs create-file-system \
--performance-mode generalPurpose \
--throughput-mode bursting \
--encrypted \
--tags Key=Name,Value=cl-storage \
--query 'FileSystemId' --output text
# Create mount targets in each private subnet
aws efs create-mount-target \
--file-system-id <efs-id> \
--subnet-id <cl-private-1-id> \
--security-groups <cl-efs-sg-id>
aws efs create-mount-target \
--file-system-id <efs-id> \
--subnet-id <cl-private-2-id> \
--security-groups <cl-efs-sg-id>
# Create an access point
aws efs create-access-point \
--file-system-id <efs-id> \
--posix-user Uid=1000,Gid=1000 \
--root-directory "Path=/data/storage,CreationInfo={OwnerUid=1000,OwnerGid=1000,Permissions=755}"
If you prefer object storage, you can use S3 with Mountpoint for Amazon S3 or modify the application to use the S3 API directly. EFS is simpler for the default POSIX filesystem approach. See Document Storage for a comparison.
Step 5: ECR Container Registry
# Create repositories
for repo in cl-backend cl-worker cl-frontend; do
aws ecr create-repository \
--repository-name contract-lucidity/$repo \
--image-scanning-configuration scanOnPush=true \
--encryption-configuration encryptionType=AES256
done
# Login to ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com
Build and push images:
cd contract-lucidity
# Backend
docker build -t cl-backend ./backend -f ./backend/Dockerfile
docker tag cl-backend:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-backend:latest
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-backend:latest
# Worker
docker build -t cl-worker ./backend -f ./backend/Dockerfile.worker
docker tag cl-worker:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-worker:latest
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-worker:latest
# Frontend
docker build -t cl-frontend ./frontend -f ./frontend/Dockerfile
docker tag cl-frontend:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-frontend:latest
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-frontend:latest
Step 6: Secrets Manager
Store sensitive configuration in AWS Secrets Manager:
aws secretsmanager create-secret \
--name contract-lucidity/config \
--secret-string '{
"POSTGRES_USER": "cl_user",
"POSTGRES_PASSWORD": "<strong-password>",
"POSTGRES_DB": "contract_lucidity",
"POSTGRES_HOST": "<rds-endpoint>",
"POSTGRES_PORT": "5432",
"REDIS_URL": "rediss://cl-redis.xxxxx.cache.amazonaws.com:6379/0",
"CELERY_BROKER_URL": "rediss://cl-redis.xxxxx.cache.amazonaws.com:6379/0",
"CELERY_RESULT_BACKEND": "rediss://cl-redis.xxxxx.cache.amazonaws.com:6379/1",
"JWT_SECRET_KEY": "<generated-secret>",
"JWT_ALGORITHM": "HS256",
"JWT_ACCESS_TOKEN_EXPIRE_MINUTES": "60",
"JWT_REFRESH_TOKEN_EXPIRE_DAYS": "7",
"DEFAULT_ADMIN_EMAIL": "admin@your-domain.com",
"DEFAULT_ADMIN_PASSWORD": "<strong-password>",
"CORS_ORIGINS": "https://your-domain.com",
"FRONTEND_URL": "https://your-domain.com",
"STORAGE_PATH": "/data/storage"
}'
When using ElastiCache with transit encryption enabled, use the rediss:// protocol (double s) in your Redis URLs.
Step 7: IAM Roles
Create two IAM roles:
ECS Task Execution Role (allows ECS to pull images and read secrets):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"logs:CreateLogStream",
"logs:PutLogEvents",
"secretsmanager:GetSecretValue"
],
"Resource": "*"
}
]
}
ECS Task Role (allows running tasks to access EFS):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"elasticfilesystem:ClientMount",
"elasticfilesystem:ClientWrite"
],
"Resource": "<efs-arn>"
}
]
}
Step 8: ECS Fargate Task Definitions
Create an ECS cluster:
aws ecs create-cluster --cluster-name cl-cluster
cl-backend Task Definition
{
"family": "cl-backend",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "<execution-role-arn>",
"taskRoleArn": "<task-role-arn>",
"volumes": [
{
"name": "cl-storage",
"efsVolumeConfiguration": {
"fileSystemId": "<efs-id>",
"transitEncryption": "ENABLED",
"authorizationConfig": {
"accessPointId": "<access-point-id>",
"iam": "ENABLED"
}
}
}
],
"containerDefinitions": [
{
"name": "cl-backend",
"image": "<account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-backend:latest",
"portMappings": [{ "containerPort": 8000, "protocol": "tcp" }],
"mountPoints": [
{ "sourceVolume": "cl-storage", "containerPath": "/data/storage" }
],
"secrets": [
{ "name": "POSTGRES_USER", "valueFrom": "<secret-arn>:POSTGRES_USER::" },
{ "name": "POSTGRES_PASSWORD", "valueFrom": "<secret-arn>:POSTGRES_PASSWORD::" },
{ "name": "POSTGRES_DB", "valueFrom": "<secret-arn>:POSTGRES_DB::" },
{ "name": "POSTGRES_HOST", "valueFrom": "<secret-arn>:POSTGRES_HOST::" },
{ "name": "POSTGRES_PORT", "valueFrom": "<secret-arn>:POSTGRES_PORT::" },
{ "name": "REDIS_URL", "valueFrom": "<secret-arn>:REDIS_URL::" },
{ "name": "CELERY_BROKER_URL", "valueFrom": "<secret-arn>:CELERY_BROKER_URL::" },
{ "name": "CELERY_RESULT_BACKEND", "valueFrom": "<secret-arn>:CELERY_RESULT_BACKEND::" },
{ "name": "JWT_SECRET_KEY", "valueFrom": "<secret-arn>:JWT_SECRET_KEY::" },
{ "name": "JWT_ALGORITHM", "valueFrom": "<secret-arn>:JWT_ALGORITHM::" },
{ "name": "JWT_ACCESS_TOKEN_EXPIRE_MINUTES", "valueFrom": "<secret-arn>:JWT_ACCESS_TOKEN_EXPIRE_MINUTES::" },
{ "name": "JWT_REFRESH_TOKEN_EXPIRE_DAYS", "valueFrom": "<secret-arn>:JWT_REFRESH_TOKEN_EXPIRE_DAYS::" },
{ "name": "DEFAULT_ADMIN_EMAIL", "valueFrom": "<secret-arn>:DEFAULT_ADMIN_EMAIL::" },
{ "name": "DEFAULT_ADMIN_PASSWORD", "valueFrom": "<secret-arn>:DEFAULT_ADMIN_PASSWORD::" },
{ "name": "CORS_ORIGINS", "valueFrom": "<secret-arn>:CORS_ORIGINS::" },
{ "name": "FRONTEND_URL", "valueFrom": "<secret-arn>:FRONTEND_URL::" },
{ "name": "STORAGE_PATH", "valueFrom": "<secret-arn>:STORAGE_PATH::" }
],
"environment": [
{ "name": "APP_ENV", "value": "production" },
{ "name": "LOG_LEVEL", "value": "INFO" },
{ "name": "MAX_UPLOAD_SIZE_MB", "value": "100" }
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/cl-backend",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}
cl-worker Task Definition
Same as cl-backend but with these changes:
{
"family": "cl-worker",
"cpu": "2048",
"memory": "4096",
"containerDefinitions": [
{
"name": "cl-worker",
"image": "<account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-worker:latest",
"portMappings": [],
"environment": [
{ "name": "APP_ENV", "value": "production" },
{ "name": "LOG_LEVEL", "value": "INFO" },
{ "name": "CELERY_CONCURRENCY", "value": "4" }
]
}
]
}
The worker performs CPU-intensive OCR and AI processing. Allocate more vCPU and memory than the backend. The CELERY_CONCURRENCY value should not exceed the vCPU count to avoid resource contention.
cl-frontend Task Definition
{
"family": "cl-frontend",
"cpu": "1024",
"memory": "2048",
"containerDefinitions": [
{
"name": "cl-frontend",
"image": "<account-id>.dkr.ecr.us-east-1.amazonaws.com/contract-lucidity/cl-frontend:latest",
"portMappings": [{ "containerPort": 3000, "protocol": "tcp" }],
"environment": [
{ "name": "BACKEND_INTERNAL_URL", "value": "http://cl-backend.cl-local:8000" },
{ "name": "NEXT_PUBLIC_FRONTEND_URL", "value": "https://your-domain.com" }
]
}
]
}
Register the task definitions:
aws ecs register-task-definition --cli-input-json file://cl-backend-task.json
aws ecs register-task-definition --cli-input-json file://cl-worker-task.json
aws ecs register-task-definition --cli-input-json file://cl-frontend-task.json
Step 9: ECS Services and Service Discovery
Create a Cloud Map namespace for internal service discovery:
aws servicediscovery create-private-dns-namespace \
--name cl-local \
--vpc <vpc-id>
Create ECS services:
# Backend service with service discovery
aws ecs create-service \
--cluster cl-cluster \
--service-name cl-backend \
--task-definition cl-backend \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[<cl-private-1-id>,<cl-private-2-id>],securityGroups=[<cl-ecs-sg-id>],assignPublicIp=DISABLED}" \
--service-registries "registryArn=<service-discovery-arn>,containerName=cl-backend,containerPort=8000"
# Worker service (no load balancer needed)
aws ecs create-service \
--cluster cl-cluster \
--service-name cl-worker \
--task-definition cl-worker \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[<cl-private-1-id>,<cl-private-2-id>],securityGroups=[<cl-ecs-sg-id>],assignPublicIp=DISABLED}"
# Frontend service
aws ecs create-service \
--cluster cl-cluster \
--service-name cl-frontend \
--task-definition cl-frontend \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[<cl-private-1-id>,<cl-private-2-id>],securityGroups=[<cl-ecs-sg-id>],assignPublicIp=DISABLED}" \
--load-balancers "targetGroupArn=<target-group-arn>,containerName=cl-frontend,containerPort=3000"
Step 10: Application Load Balancer
# Create ALB
aws elbv2 create-load-balancer \
--name cl-alb \
--subnets <cl-public-1-id> <cl-public-2-id> \
--security-groups <cl-alb-sg-id> \
--scheme internet-facing \
--type application
# Create target group
aws elbv2 create-target-group \
--name cl-frontend-tg \
--protocol HTTP \
--port 3000 \
--vpc-id <vpc-id> \
--target-type ip \
--health-check-path "/" \
--health-check-interval-seconds 30
# Request SSL certificate
aws acm request-certificate \
--domain-name your-domain.com \
--validation-method DNS
# Create HTTPS listener (after certificate is validated)
aws elbv2 create-listener \
--load-balancer-arn <alb-arn> \
--protocol HTTPS \
--port 443 \
--certificates CertificateArn=<certificate-arn> \
--default-actions Type=forward,TargetGroupArn=<target-group-arn>
# Create HTTP → HTTPS redirect
aws elbv2 create-listener \
--load-balancer-arn <alb-arn> \
--protocol HTTP \
--port 80 \
--default-actions Type=redirect,RedirectConfig='{Protocol=HTTPS,Port=443,StatusCode=HTTP_301}'
Step 11: DNS
Point your domain to the ALB:
# If using Route 53
aws route53 change-resource-record-sets \
--hosted-zone-id <zone-id> \
--change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "your-domain.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "<alb-hosted-zone-id>",
"DNSName": "<alb-dns-name>",
"EvaluateTargetHealth": true
}
}
}]
}'
Cost Estimate
Estimated monthly costs (as of March 2026) for a production deployment in US East (N. Virginia):
| Service | Specification | Estimated Monthly Cost |
|---|---|---|
| ECS Fargate (frontend) | 2 tasks x 1 vCPU / 2 GB | ~$60 |
| ECS Fargate (backend) | 2 tasks x 1 vCPU / 2 GB | ~$60 |
| ECS Fargate (worker) | 1 task x 2 vCPU / 4 GB | ~$60 |
| RDS PostgreSQL | db.t3.medium, Multi-AZ, 50 GB gp3 | ~$140 |
| ElastiCache Redis | cache.t3.small, 2 nodes | ~$50 |
| EFS | 10 GB (scales automatically) | ~$3 |
| ALB | 1 ALB + data processing | ~$25 |
| ECR | Image storage | ~$2 |
| Secrets Manager | 1 secret | ~$1 |
| Data Transfer | Moderate usage | ~$10 |
| CloudWatch Logs | Log storage | ~$5 |
| Total | ~$416/month |
- Use Fargate Spot for the worker (up to 70% savings for interruptible workloads). Document processing tasks are retried automatically.
- Use Reserved Instances for RDS and ElastiCache (up to 55% savings with 1-year commitment).
- Use Savings Plans for Fargate compute (up to 50% savings).
- Use ARM/Graviton instances where possible for a 20% price reduction. The Python backend and Node.js frontend both run on ARM.
Auto-Scaling
Configure auto-scaling for the frontend and backend services:
# Register scalable target
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/cl-cluster/cl-frontend \
--min-capacity 2 \
--max-capacity 10
# Create scaling policy (target 70% CPU)
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/cl-cluster/cl-frontend \
--policy-name cl-frontend-cpu \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
},
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60
}'
Verification
# Check ECS services
aws ecs describe-services --cluster cl-cluster \
--services cl-backend cl-worker cl-frontend \
--query 'services[].{name:serviceName,status:status,running:runningCount,desired:desiredCount}'
# Check ALB health
aws elbv2 describe-target-health --target-group-arn <target-group-arn>
# Test the application
curl -I https://your-domain.com
Verify current pricing and service availability at aws.amazon.com/pricing. AWS pricing changes frequently and may vary by region.