Skip to main content

Deploying to AWS

A CloudFormation template that runs OrbisID on ECS Fargate against an RDS PostgreSQL database, pulling the official orbisid/orbisid-backend and orbisid/orbisid-frontend images directly from Docker Hub — no ECR, no image build step.

Quick launch (no domain required)​

If you just want to try OrbisID without owning a domain or an ACM certificate, AWS Marketplace also offers OrbisID as a self-contained EC2 AMI with 1-Click Launch: launch the AMI, wait a few minutes, and browse to the URL shown in the instance's system log. It generates its own database password, encryption key, and a self-signed TLS certificate on first boot — no parameters to fill in. The tradeoffs versus the CloudFormation deployment below: a single EC2 instance rather than ECS/RDS, a self-signed certificate (browser warning) instead of a trusted one, and all data lives on that instance's own EBS volume rather than a managed database. Find it on AWS Marketplace as "OrbisID Community Edition — Quick Launch"; the rest of this page covers the production-oriented CloudFormation/ECS path.

Architecture​

This mirrors exactly what nginx.conf does for a Docker Compose install (/api/ and /actuator/ to the backend, everything else to the frontend) — just implemented as ALB listener rules instead of an nginx container, so there's no config file to ship into the container image.

Prerequisites​

  • AWS CLI installed and configured (aws configure)
  • A domain name you control
  • An ACM certificate for that domain, already issued and validated, in the same region you're deploying to. OrbisID's session cookies are Secure-only (see application.yml), so HTTPS at the load balancer isn't optional the way it is for a LAN-only Docker Compose install — request the certificate first:
aws acm request-certificate --domain-name orbisid.example.com --validation-method DNS

Then add the returned CNAME validation record to your DNS and wait for Status: ISSUED (aws acm describe-certificate --certificate-arn <arn>).

Deploy​

Copy the template below and save it locally as cloudformation.yaml:

cloudformation.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: >
OrbisID on AWS — VPC, RDS PostgreSQL, ECS Fargate (backend + frontend services
pulling the official Docker Hub images), and an Application Load Balancer that
path-routes /api/* and /actuator/* to the backend and everything else to the
frontend (the same split docs-site/nginx.conf does for a Docker Compose install).
See docs-site/docs/installation/cloud-aws.md for the full walkthrough.

Parameters:
EnvironmentName:
Type: String
Default: orbisid
Description: Prefix used to name every resource this stack creates.

OrbisIDVersion:
Type: String
Default: latest
Description: >
Tag to pull from orbisid/orbisid-backend and orbisid/orbisid-frontend on
Docker Hub. Pin to a specific released version (e.g. 2.12.2) for production
— "latest" will change under you on the next image push.

CertificateArn:
Type: String
AllowedPattern: '^arn:aws:acm:.*'
Description: >
ACM certificate ARN for your domain, in the same region as this stack.
OrbisID's session cookies are Secure-only, so HTTPS at the load balancer
is required, not optional — request/validate the certificate before
deploying this stack.

DBUsername:
Type: String
Default: orbisid
Description: Master username for the RDS PostgreSQL instance.

DBPassword:
Type: String
NoEcho: true
MinLength: 8
Description: Master password for the RDS PostgreSQL instance.

DBName:
Type: String
Default: orbisid

DBInstanceClass:
Type: String
Default: db.t4g.micro
Description: See requirements.md — 2 vCPU / 8 GB is the recommended baseline for the whole stack; scale this and the Fargate sizes together.

DBAllocatedStorage:
Type: Number
Default: 20
MinValue: 20

DBMultiAZ:
Type: String
Default: 'false'
AllowedValues: ['true', 'false']
Description: Set to true for production high availability (roughly doubles RDS cost).

EncryptionKey:
Type: String
NoEcho: true
Description: >
AES-256-GCM key OrbisID uses to encrypt stored credentials. Generate with
`openssl rand -base64 32`. Keep this safe outside of AWS too — if it is
lost, encrypted data cannot be recovered.

BackendCpu:
Type: Number
Default: 1024
Description: Fargate CPU units for the backend task (1024 = 1 vCPU).

BackendMemory:
Type: Number
Default: 2048
Description: Fargate memory (MiB) for the backend task.

FrontendCpu:
Type: Number
Default: 512

FrontendMemory:
Type: Number
Default: 1024

DesiredCount:
Type: Number
Default: 1
Description: Number of tasks per service. Increase for HA once Application Auto Scaling is wired up (not included in this base template).

VpcCidr:
Type: String
Default: 10.0.0.0/16

Resources:
# ── Networking ────────────────────────────────────────────────────────────
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-vpc'

InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-igw'

VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway

PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Select [0, !Cidr [!Ref VpcCidr, 4, 8]]
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-public-1'

PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Select [1, !Cidr [!Ref VpcCidr, 4, 8]]
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-public-2'

PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Select [2, !Cidr [!Ref VpcCidr, 4, 8]]
AvailabilityZone: !Select [0, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-private-1'

PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Select [3, !Cidr [!Ref VpcCidr, 4, 8]]
AvailabilityZone: !Select [1, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-private-2'

PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-public-rt'

PublicRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway

PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable

PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable

NatEIP:
Type: AWS::EC2::EIP
DependsOn: VPCGatewayAttachment
Properties:
Domain: vpc

# Single NAT Gateway to keep the base cost down — see docs-site/docs/installation/cloud-aws.md
# for how to add a second one (one per AZ) for NAT high availability.
NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatEIP.AllocationId
SubnetId: !Ref PublicSubnet1
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-nat'

PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-private-rt'

PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway

PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable

PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable

# ── Security Groups ──────────────────────────────────────────────────────
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub '${EnvironmentName} ALB - public HTTP/HTTPS'
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-alb-sg'

BackendSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub '${EnvironmentName} backend tasks - ALB only'
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 8080
ToPort: 8080
SourceSecurityGroupId: !Ref ALBSecurityGroup
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-backend-sg'

FrontendSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub '${EnvironmentName} frontend tasks - ALB only'
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3000
ToPort: 3000
SourceSecurityGroupId: !Ref ALBSecurityGroup
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-frontend-sg'

DBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub '${EnvironmentName} RDS - backend tasks only'
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId: !Ref BackendSecurityGroup
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-db-sg'

# ── Secrets ──────────────────────────────────────────────────────────────
DBPasswordSecret:
Type: AWS::SecretsManager::Secret
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
Name: !Sub '${EnvironmentName}/db-password'
SecretString: !Ref DBPassword

# Retained even if the stack is deleted — losing this key makes all encrypted
# credentials in the database unrecoverable, so an accidental stack deletion
# must not take it with it.
EncryptionKeySecret:
Type: AWS::SecretsManager::Secret
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
Name: !Sub '${EnvironmentName}/encryption-key'
SecretString: !Ref EncryptionKey

# ── Database ─────────────────────────────────────────────────────────────
DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: !Sub '${EnvironmentName} RDS subnet group (private subnets)'
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2

RDSInstance:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
UpdateReplacePolicy: Snapshot
Properties:
DBInstanceIdentifier: !Sub '${EnvironmentName}-postgres'
Engine: postgres
# No EngineVersion pinned deliberately — RDS's default postgres version for
# new instances changes over time and a hardcoded minor version here would
# eventually be rejected as deprecated. Requires PostgreSQL 15+ (see
# requirements.md); check `aws rds describe-db-engine-versions --engine
# postgres` if you need to pin a specific version instead.
DBInstanceClass: !Ref DBInstanceClass
AllocatedStorage: !Ref DBAllocatedStorage
StorageType: gp3
StorageEncrypted: true
MasterUsername: !Ref DBUsername
MasterUserPassword: !Ref DBPassword
DBName: !Ref DBName
DBSubnetGroupName: !Ref DBSubnetGroup
VPCSecurityGroups:
- !Ref DBSecurityGroup
MultiAZ: !Ref DBMultiAZ
PubliclyAccessible: false
BackupRetentionPeriod: 7
CopyTagsToSnapshot: true

# ── ECS ──────────────────────────────────────────────────────────────────
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub '${EnvironmentName}-cluster'
ClusterSettings:
- Name: containerInsights
Value: enabled

LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/ecs/${EnvironmentName}'
RetentionInDays: 30

ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${EnvironmentName}-ecs-execution-role'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Policies:
- PolicyName: ReadOrbisIDSecrets
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource:
- !Ref DBPasswordSecret
- !Ref EncryptionKeySecret

BackendTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub '${EnvironmentName}-backend'
Cpu: !Ref BackendCpu
Memory: !Ref BackendMemory
NetworkMode: awsvpc
RequiresCompatibilities: [FARGATE]
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
ContainerDefinitions:
- Name: backend
Image: !Sub 'orbisid/orbisid-backend:${OrbisIDVersion}'
Essential: true
PortMappings:
- ContainerPort: 8080
Environment:
- Name: SPRING_PROFILES_ACTIVE
Value: docker
- Name: SPRING_DATASOURCE_URL
Value: !Sub 'jdbc:postgresql://${RDSInstance.Endpoint.Address}:${RDSInstance.Endpoint.Port}/${DBName}'
- Name: SPRING_DATASOURCE_USERNAME
Value: !Ref DBUsername
Secrets:
- Name: SPRING_DATASOURCE_PASSWORD
ValueFrom: !Ref DBPasswordSecret
- Name: ORBISID_ENCRYPTION_KEY
ValueFrom: !Ref EncryptionKeySecret
HealthCheck:
Command:
- CMD-SHELL
- wget --quiet --tries=1 --spider http://localhost:8080/actuator/health || exit 1
Interval: 30
Timeout: 10
Retries: 3
StartPeriod: 90
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: backend

FrontendTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub '${EnvironmentName}-frontend'
Cpu: !Ref FrontendCpu
Memory: !Ref FrontendMemory
NetworkMode: awsvpc
RequiresCompatibilities: [FARGATE]
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
ContainerDefinitions:
- Name: frontend
Image: !Sub 'orbisid/orbisid-frontend:${OrbisIDVersion}'
Essential: true
PortMappings:
- ContainerPort: 3000
Environment:
# Frontend calls the API via a relative path — it and the backend
# are served from the same ALB/domain, routed by path (see the
# listener rule below), so no absolute URL is needed here.
- Name: NEXT_PUBLIC_API_URL
Value: ''
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: frontend

BackendService:
Type: AWS::ECS::Service
DependsOn: HTTPSListener
Properties:
ServiceName: !Sub '${EnvironmentName}-backend'
Cluster: !Ref ECSCluster
TaskDefinition: !Ref BackendTaskDefinition
DesiredCount: !Ref DesiredCount
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
Subnets:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
SecurityGroups:
- !Ref BackendSecurityGroup
LoadBalancers:
- ContainerName: backend
ContainerPort: 8080
TargetGroupArn: !Ref BackendTargetGroup
HealthCheckGracePeriodSeconds: 90

FrontendService:
Type: AWS::ECS::Service
DependsOn: HTTPSListener
Properties:
ServiceName: !Sub '${EnvironmentName}-frontend'
Cluster: !Ref ECSCluster
TaskDefinition: !Ref FrontendTaskDefinition
DesiredCount: !Ref DesiredCount
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
Subnets:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
SecurityGroups:
- !Ref FrontendSecurityGroup
LoadBalancers:
- ContainerName: frontend
ContainerPort: 3000
TargetGroupArn: !Ref FrontendTargetGroup

# ── Load Balancer ────────────────────────────────────────────────────────
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub '${EnvironmentName}-alb'
Scheme: internet-facing
Type: application
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
SecurityGroups:
- !Ref ALBSecurityGroup

BackendTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub '${EnvironmentName}-backend-tg'
Port: 8080
Protocol: HTTP
TargetType: ip
VpcId: !Ref VPC
HealthCheckPath: /actuator/health
HealthCheckIntervalSeconds: 30
HealthyThresholdCount: 2
UnhealthyThresholdCount: 3
Matcher:
HttpCode: '200'

FrontendTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub '${EnvironmentName}-frontend-tg'
Port: 3000
Protocol: HTTP
TargetType: ip
VpcId: !Ref VPC
HealthCheckPath: /
HealthCheckIntervalSeconds: 30
HealthyThresholdCount: 2
UnhealthyThresholdCount: 3
Matcher:
HttpCode: '200-399'

HTTPSListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 443
Protocol: HTTPS
Certificates:
- CertificateArn: !Ref CertificateArn
DefaultActions:
- Type: forward
TargetGroupArn: !Ref FrontendTargetGroup

HTTPListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- Type: redirect
RedirectConfig:
Protocol: HTTPS
Port: '443'
StatusCode: HTTP_301

BackendListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref HTTPSListener
Priority: 10
Conditions:
- Field: path-pattern
Values:
- /api/*
- /actuator/*
Actions:
- Type: forward
TargetGroupArn: !Ref BackendTargetGroup

Outputs:
ApplicationURL:
Description: OrbisID URL (point your domain's DNS at LoadBalancerDNSName, then browse here)
Value: !Sub 'https://${ApplicationLoadBalancer.DNSName}'

LoadBalancerDNSName:
Description: Create a CNAME/ALIAS record for your domain pointing at this
Value: !GetAtt ApplicationLoadBalancer.DNSName

DatabaseEndpoint:
Description: RDS PostgreSQL endpoint (private — only reachable from the backend tasks)
Value: !GetAtt RDSInstance.Endpoint.Address

ECSClusterName:
Value: !Ref ECSCluster

VpcId:
Value: !Ref VPC

Generate a database password and an encryption key, then deploy the stack:

DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9')
ENCRYPTION_KEY=$(openssl rand -base64 32)
echo "Save this — it cannot be recovered if lost: $ENCRYPTION_KEY"

aws cloudformation deploy \
--template-file cloudformation.yaml \
--stack-name orbisid \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxx \
DBPassword="$DB_PASSWORD" \
EncryptionKey="$ENCRYPTION_KEY"

Or deploy through the Console instead: CloudFormation > Create stack > With new resources (standard) > Choose an existing template > Upload a template file, select cloudformation.yaml, and fill in the parameters (CertificateArn is required; generate DBPassword/EncryptionKey the same way as above and paste them in).

It takes 10–15 minutes, mostly for the RDS instance and NAT gateway.

Once the stack reaches CREATE_COMPLETE, fetch the outputs:

aws cloudformation describe-stacks --stack-name orbisid --query 'Stacks[0].Outputs' --output table

When it finishes, LoadBalancerDNSName is the output you need next.

Other parameters worth knowing about (all have defaults except CertificateArn):

ParameterDefaultPurpose
CertificateArnrequiredACM certificate ARN
EnvironmentNameorbisidPrefix for every resource name
OrbisIDVersionlatestDocker Hub image tag to deploy — pin this to a specific release (e.g. 2.12.2) for production, since latest moves under you
DBInstanceClassdb.t4g.microRDS instance size
DBMultiAZfalseSet true for production high availability
BackendCpu / BackendMemory1024 / 2048Fargate sizing for the backend task
DesiredCount1Tasks per service

Post-deployment​

  1. Create a CNAME (or ALIAS, if using Route 53) record for your domain pointing at LoadBalancerDNSName.
  2. Browse to https://your-domain. Log in with the default credentials (admin / ChangeMe123!) and change the password immediately — Administration > Users.
  3. Add target systems and configure scanning as usual — see Systems and Scanning.

Cost (rough estimate)​

With the template's defaults (single AZ pair, one NAT gateway, db.t4g.micro, one task each for backend/frontend, single-AZ RDS): roughly $120–160/month in us-east-1. This is a rough order of magnitude, not a quote — check the AWS Pricing Calculator for your region and traffic. The main levers if you need to cut cost further or scale up: DBInstanceClass, BackendCpu/BackendMemory, DesiredCount, and DBMultiAZ.

Updating​

Redeploy with the same parameters plus a new OrbisIDVersion — aws cloudformation deploy requires every parameter without a default (CertificateArn, DBPassword, EncryptionKey) on every call, so keep those three somewhere you can reuse them (a password manager, or a local, git-ignored parameters file):

aws cloudformation deploy \
--template-file cloudformation.yaml \
--stack-name orbisid \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
CertificateArn="$CERTIFICATE_ARN" \
DBPassword="$DB_PASSWORD" \
EncryptionKey="$ENCRYPTION_KEY" \
OrbisIDVersion=2.13.0

aws cloudformation deploy only touches what changed — the RDS instance and its data are untouched by an image version bump. Database migrations run automatically on backend startup (Flyway), same as the Docker Compose install.

Known limitations​

  • No Kafka. Endpoint Sensor / Threat Detection event ingestion (KAFKA_BOOTSTRAP_SERVERS) needs a reachable Kafka broker, which this template doesn't provision — same as the released Docker Compose packages, which also ship without one. If you need that feature in this deployment, add a managed Kafka-compatible service (Amazon MSK Serverless is the natural fit in-VPC) and set KAFKA_BOOTSTRAP_SERVERS on the backend task definition.
  • No bundled AI runtime. This template doesn't run Ollama — AI features stay off. To enable them, either point Administration > Settings > OrbisAI's base URL at a separately-hosted Ollama instance reachable from the backend's private subnet, or extend the template with an additional Fargate service running ollama/ollama plus an EFS volume for model storage (CPU inference is memory-hungry — see Requirements).
  • No autoscaling. DesiredCount is fixed; wire up Application Auto Scaling on the ECS services if you need it.
  • Single NAT gateway. A NAT outage in that AZ takes down internet egress for both private subnets. Add a second NAT gateway (one per AZ) for NAT high availability if this matters to you.

Tearing down​

aws cloudformation delete-stack --stack-name orbisid --region <region>

The DBPasswordSecret and EncryptionKeySecret Secrets Manager secrets are retained on stack deletion by design (so an accidental delete can't take your encryption key with it) — delete them manually afterwards if you're done with them for good:

aws secretsmanager delete-secret --secret-id orbisid/db-password --force-delete-without-recovery
aws secretsmanager delete-secret --secret-id orbisid/encryption-key --force-delete-without-recovery
Irreversible

--force-delete-without-recovery skips Secrets Manager's recovery window. The RDS instance is deleted with a final snapshot (DeletionPolicy: Snapshot) — the snapshot itself isn't removed automatically and will keep costing storage until you delete it too.

Troubleshooting​

  • ECS service stuck, tasks cycling: check aws logs tail /ecs/orbisid --follow — usually a database connectivity issue (security group) or a bad ENCRYPTION_KEY/DBPassword value.
  • ALB health checks failing on the backend target group: the backend takes time to run Flyway migrations on first boot; the target group's grace period (90s) usually covers it, but a large migration backlog on first deploy can exceed that — watch the ECS service events in the console.
  • 502/504 from the ALB: almost always the frontend or backend task isn't healthy yet, or the security group rule from the ALB to the task's port is missing (shouldn't happen with the template as shipped, but check first if you customised it).