AWS Deployment Runbook - VM Attached Storage

January 4, 2026 · View on GitHub

Deployment Type: vm-attached-storage

This deployment type creates a single EC2 instance running all Keystone services (PostgreSQL, Keycloak, Backend, Frontend) with an attached EBS volume for data persistence.

Best suited for:

  • Small to medium deployments (< 100 concurrent users)
  • Development/staging environments
  • Cost-conscious production deployments
  • Single-region deployments

Estimated cost: ~$33/month (eu-west-3)


(i) Prerequisites

AWS Resources (Manual Setup)

ResourcePurposeHow to Create
AWS AccountInfrastructure hostingAWS Console
Elastic IPStatic public IPaws ec2 allocate-address --domain vpc --region eu-west-3
S3 BucketSSL certificate persistenceSee below
Domain NamePublic access URLPurchase via registrar, point A record to Elastic IP

Create S3 Bucket for SSL Certificates

# Create bucket (name must be globally unique)
aws s3 mb s3://your-caddy-certs-bucket --region eu-west-3

# Enable versioning (recommended)
aws s3api put-bucket-versioning \
  --bucket your-caddy-certs-bucket \
  --versioning-configuration Status=Enabled

# Block public access
aws s3api put-public-access-block \
  --bucket your-caddy-certs-bucket \
  --public-access-block-configuration \
  "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

Important: Create the S3 bucket manually (outside Terraform) so certificates survive infrastructure destroy/recreate cycles.

Local Tools Required

ToolVersionInstallation
AWS CLI2.xbrew install awscli or AWS docs
OpenTofu1.6+brew install opentofu
Git2.xbrew install git
Make3.x+Pre-installed on macOS/Linux

AWS CLI Configuration

aws configure
# AWS Access Key ID: <your-key>
# AWS Secret Access Key: <your-secret>
# Default region name: eu-west-3
# Default output format: json

Required AWS Permissions

The IAM user/role needs permissions for:

  • EC2 (instances, volumes, security groups, key pairs)
  • VPC (VPCs, subnets, internet gateways, route tables)
  • IAM (roles, instance profiles, policies)
  • S3 (for certificate bucket access)
  • SSM (for Session Manager access)

(ii) Architecture

Infrastructure Diagram

┌─────────────────────────────────────────────────────────────────────┐
│                           AWS Cloud                                  │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │                    VPC (10.0.0.0/16)                          │  │
│  │  ┌─────────────────────────────────────────────────────────┐  │  │
│  │  │              Public Subnet (10.0.1.0/24)                │  │  │
│  │  │                                                         │  │  │
│  │  │  ┌─────────────────────────────────────────────────┐   │  │  │
│  │  │  │           EC2 Instance (t3.medium)              │   │  │  │
│  │  │  │                                                 │   │  │  │
│  │  │  │  ┌─────────────────────────────────────────┐   │   │  │  │
│  │  │  │  │              Docker                      │   │   │  │  │
│  │  │  │  │  ┌─────────┐ ┌─────────┐ ┌─────────┐   │   │   │  │  │
│  │  │  │  │  │PostgreSQL│ │Keycloak │ │ Backend │   │   │   │  │  │
│  │  │  │  │  │  :5432   │ │  :8080  │ │  :8000  │   │   │   │  │  │
│  │  │  │  │  └─────────┘ └─────────┘ └─────────┘   │   │   │  │  │
│  │  │  │  │  ┌─────────┐                           │   │   │  │  │
│  │  │  │  │  │Frontend │                           │   │   │  │  │
│  │  │  │  │  │  :3000  │                           │   │   │  │  │
│  │  │  │  │  └─────────┘                           │   │   │  │  │
│  │  │  │  └─────────────────────────────────────────┘   │   │  │  │
│  │  │  │                                                 │   │  │  │
│  │  │  │  ┌─────────────────────────────────────────┐   │   │  │  │
│  │  │  │  │    Caddy (Reverse Proxy + SSL)          │   │   │  │  │
│  │  │  │  │    :80, :443 → Backend/Keycloak/Frontend│   │   │  │  │
│  │  │  │  └─────────────────────────────────────────┘   │   │  │  │
│  │  │  └─────────────────────────────────────────────────┘   │  │  │
│  │  │           │                                             │  │  │
│  │  │           │ /dev/nvme1n1                               │  │  │
│  │  │           ▼                                             │  │  │
│  │  │  ┌─────────────────┐                                   │  │  │
│  │  │  │   EBS Volume    │                                   │  │  │
│  │  │  │   20GB gp3      │                                   │  │  │
│  │  │  │ /data/postgres/ │                                   │  │  │
│  │  │  └─────────────────┘                                   │  │  │
│  │  └─────────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                                                                      │
│  ┌────────────┐    ┌────────────────┐    ┌─────────────────────┐   │
│  │ Elastic IP │    │ Security Group │    │    S3 Bucket        │   │
│  │ (static)   │    │ 80, 443 open   │    │ (SSL certificates)  │   │
│  └────────────┘    └────────────────┘    └─────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

                              │ HTTPS :443

                     ┌─────────────────┐
                     │   Internet      │
                     │   Users         │
                     └─────────────────┘

Components

ComponentTechnologyPortPurpose
Reverse ProxyCaddy80, 443SSL termination, routing
FrontendNext.js 153000Web UI
BackendFastAPI + Agno8000AI agent, API
AuthKeycloak8080OAuth2/OIDC
DatabasePostgreSQL 16 + PgVector5432Data storage

Data Persistence

DataLocationPersistence
PostgreSQL data/data/postgres/pgdata (EBS)Survives instance termination
SSL certificatesS3 bucketSurvives infrastructure destroy
Application code/opt/keystone (root volume)Rebuilt on deploy
Docker imagesRoot volumeRebuilt on deploy

Network Flow

User Request → Elastic IP → Caddy (:443)

                              ├── /realms/* → Keycloak (:8080)
                              ├── /resources/* → Keycloak (:8080)
                              ├── /api/* → Backend (:8000)
                              ├── /agui/* → Backend (:8000)
                              └── /* → Frontend (:3000)

(iii) Deploy From Scratch

Step 1: Clone Repository

git clone git@github.com:fulltechfactory/keystone.git
cd keystone

Step 2: Configure Production Environment

make setup-deploy

Answer the prompts:

PromptExample ValueNotes
Cloud providerawsOnly AWS supported currently
Infrastructure typevm-attached-storageSingle VM deployment
Domain namewww.example.comMust point to your Elastic IP
Existing Elastic IP?yRecommended
Elastic IP allocation IDeipalloc-xxxFrom AWS console
AI provideropenaiRequired for RAG embeddings
AI API keysk-...Your OpenAI API key
Database passwords(generate strong)No @, :, /, # characters
Keystone admin password(generate strong)For adminuser login
Keycloak admin password(generate strong)For Keycloak console
Caddy S3 bucket nameyour-caddy-certs-bucketCreated in prerequisites

Step 3: Initialize OpenTofu

make infra-init

Step 4: Preview Infrastructure

make infra-plan

Review the plan. Expected resources:

  • 1 VPC + subnet + internet gateway + route table
  • 1 Security group (ports 80, 443)
  • 1 IAM role + instance profile
  • 1 EC2 instance (t3.medium)
  • 1 EBS volume (20GB)
  • 1 Elastic IP association

Step 5: Deploy Infrastructure

make infra-apply

Deployment takes ~5-10 minutes.

Step 6: Monitor Deployment

# Get instance ID
make infra-output

# Connect via SSM
aws ssm start-session --target <instance-id> --region eu-west-3

# Watch deployment logs
sudo tail -f /var/log/user-data.log

Wait for: === Keystone setup complete ===

Step 7: Verify Deployment

# On the server
sudo docker ps  # All 4 containers running
sudo systemctl status caddy  # Caddy active

# From your machine
curl -I https://your-domain.com  # Should return 200

Step 8: Access Application

  1. Open https://your-domain.com
  2. Sign in with adminuser / <your-keystone-admin-password>
  3. Create groups, users, and start using the application

(iv) Update Procedures

Update Backend Only

# Connect to server
aws ssm start-session --target <instance-id> --region eu-west-3

# Update
cd /opt/keystone
sudo git pull
sudo docker compose build --no-cache backend
sudo docker compose up -d backend

# Verify
sudo docker logs -f keystone-backend

Update Frontend Only

# Connect to server
aws ssm start-session --target <instance-id> --region eu-west-3

# Update
cd /opt/keystone
sudo git pull
sudo docker compose build --no-cache frontend
sudo docker compose up -d frontend

# Verify
sudo docker logs -f keystone-frontend

Update All Services

# Connect to server
aws ssm start-session --target <instance-id> --region eu-west-3

# Update
cd /opt/keystone
sudo git pull
sudo docker compose down
sudo docker compose build --no-cache
sudo docker compose up -d

# Verify
sudo docker ps
sudo docker compose logs -f

Update Infrastructure (OpenTofu)

# On local machine
cd keystone
git pull
make infra-plan  # Review changes
make infra-apply  # Apply changes

Warning: Some infrastructure changes may cause downtime or data loss. Always review the plan carefully.

Rollback Procedure

# Connect to server
aws ssm start-session --target <instance-id> --region eu-west-3

# Rollback to specific commit
cd /opt/keystone
sudo git fetch
sudo git checkout <commit-hash>
sudo docker compose down
sudo docker compose build --no-cache
sudo docker compose up -d

(v) Troubleshooting Runbook

Quick Health Check

# Connect to server
aws ssm start-session --target <instance-id> --region eu-west-3

# Check all services
sudo docker ps
sudo systemctl status caddy
df -h /data/postgres  # EBS volume mounted?
curl -s http://localhost:8000/health | jq  # Backend healthy?
curl -s http://localhost:3000  # Frontend responding?

Problem: Cannot Connect to Server

Symptoms: SSM session fails, SSH times out

Investigation:

# Check instance status
aws ec2 describe-instance-status --instance-ids <instance-id> --region eu-west-3

# Check if instance is running
aws ec2 describe-instances --instance-ids <instance-id> --region eu-west-3 \
  --query 'Reservations[0].Instances[0].State.Name'

Solutions:

  1. Instance stopped → Start via AWS console
  2. Instance terminated → Run make infra-apply to recreate
  3. SSM agent not running → Reboot instance via AWS console

Problem: Website Not Loading (Connection Refused)

Symptoms: Browser shows "Connection refused" or timeout

Investigation:

# On server
sudo systemctl status caddy
sudo journalctl -u caddy -n 50
sudo docker ps

Solutions:

CauseSolution
Caddy not runningsudo systemctl start caddy
Caddy config errorCheck /etc/caddy/Caddyfile, then sudo systemctl restart caddy
Security groupVerify ports 80, 443 are open in AWS console
Docker not runningsudo systemctl start docker && cd /opt/keystone && sudo docker compose up -d

Problem: SSL Certificate Error

Symptoms: Browser shows "Invalid certificate" or "Not secure"

Investigation:

sudo journalctl -u caddy -n 100 | grep -i "certificate\|acme\|tls"

Solutions:

CauseSolution
Rate limited (429)Wait 1 week, certificates are cached in S3
DNS not propagatedVerify A record: dig +short your-domain.com
S3 access deniedCheck IAM role has S3 permissions
Wrong domainVerify domain_name in .deploy-config

Problem: "Sign in with Keycloak" Error

Symptoms: Login redirects to error page

Investigation:

sudo docker logs keystone-keycloak 2>&1 | tail -50
sudo docker logs keystone-keycloak-setup 2>&1

Solutions:

CauseSolution
Keycloak not readyWait 1-2 minutes, check sudo docker ps
Redirect URI mismatchCheck Keycloak admin console → Clients → keystone-app → Valid Redirect URIs
Database connectionCheck PostgreSQL: sudo docker logs keystone-postgres

Problem: Keycloak Cannot Connect to Database

Symptoms: password authentication failed for user "keycloak"

Investigation:

sudo docker logs keystone-keycloak 2>&1 | grep -i "password\|authentication\|database"

Solutions:

# Reset Keycloak password in PostgreSQL
source /opt/keystone/.deploy-config
sudo docker exec keystone-postgres psql -U postgres -c \
  "ALTER USER keycloak WITH PASSWORD '$DB_KEYCLOAK_PASSWORD';"
sudo docker compose restart keycloak

Problem: RAG Not Working

Symptoms: Chat doesn't find documents, "No relevant information found"

Investigation:

# Check backend logs
sudo docker logs keystone-backend 2>&1 | grep -i "knowledge\|search\|error"

# Check if documents exist
sudo docker exec keystone-postgres psql -U postgres -d keystone_db -c \
  "SELECT COUNT(*) FROM app.knowledge_embeddings;"

Solutions:

CauseSolution
No documents uploadedUpload documents via Knowledge Base UI
OpenAI API key invalidCheck .deploy-config, restart backend
User not in groupAdd user to group with documents
Missing permissionsGrant READ/WRITE permission in KB Management

Problem: PostgreSQL Data Lost

Symptoms: Users, groups, documents missing after restart

Investigation:

# Check EBS volume mounted
df -h /data/postgres
ls -la /data/postgres/pgdata/

# Check docker-compose override
cat /opt/keystone/docker-compose.override.yml | grep -A3 postgres

Solutions:

CauseSolution
EBS not mountedsudo mount /dev/nvme1n1 /data/postgres
Using Docker volumeFix docker-compose.override.yml to use /data/postgres/pgdata
Data corruptedRestore from backup (if available)

Problem: High Memory/CPU Usage

Symptoms: Application slow, instance unresponsive

Investigation:

htop  # or top
sudo docker stats
df -h  # Disk full?

Solutions:

CauseSolution
Memory exhaustedUpgrade to larger instance type
Disk fullClean Docker: sudo docker system prune -a
Runaway processsudo docker compose restart

Useful Commands Reference

# Service Management
sudo docker compose up -d          # Start all services
sudo docker compose down           # Stop all services
sudo docker compose restart        # Restart all services
sudo docker compose logs -f        # Follow all logs

# Individual Service Logs
sudo docker logs keystone-postgres
sudo docker logs keystone-keycloak
sudo docker logs keystone-backend
sudo docker logs keystone-frontend
sudo journalctl -u caddy -f

# Database Access
sudo docker exec -it keystone-postgres psql -U postgres -d keystone_db

# Check Configurations
cat /opt/keystone/.deploy-config
cat /opt/keystone/docker-compose.override.yml
cat /etc/caddy/Caddyfile

# Disk and Memory
df -h
free -m
sudo docker system df

Appendix: File Locations

FileLocationPurpose
Application code/opt/keystone/Git repository
Deploy config/opt/keystone/.deploy-configEnvironment variables
Docker override/opt/keystone/docker-compose.override.ymlProduction overrides
PostgreSQL data/data/postgres/pgdata/Database files (EBS)
Caddy config/etc/caddy/CaddyfileReverse proxy config
Caddy binary/usr/bin/caddyCustom build with S3
User-data log/var/log/user-data.logDeployment log
SSL certificatesS3 bucketManaged by Caddy

Appendix: Port Reference

PortServiceAccess
22SSHDisabled (use SSM)
80Caddy HTTPPublic (redirects to 443)
443Caddy HTTPSPublic
3000FrontendInternal only
5432PostgreSQLInternal only
8000BackendInternal only
8080KeycloakInternal only