Skip to content

Latest commit

 

History

History
531 lines (437 loc) · 19.6 KB

DEPLOY.md

File metadata and controls

531 lines (437 loc) · 19.6 KB

AgentKit AWS EC2 Deployment Guide

This guide provides step-by-step instructions for deploying a production-ready AgentKit instance on a single AWS EC2 virtual machine using Docker Compose.


Table of Contents

  1. Target Architecture
  2. EC2 Instance Sizing & Provisioning
  3. AWS Security Group Configuration
  4. Host Setup & Docker Installation
  5. Storage & Volume Persistence
  6. Environment & Secrets Management
  7. Systemd Service Daemon
  8. Reverse Proxy & Automatic SSL Termination
  9. Deployment & Verification Checklist
  10. Monitoring, Backups & Maintenance

1. Target Architecture

                             Internet
                                │
                                ▼
                       [AWS Security Group]
                   (Ports 80 & 443 Public; 22 Restricted)
                                │
                                ▼
                 ┌─────────────────────────────┐
                 │       EC2 Host (Ubuntu)     │
                 │                             │
                 │   [Reverse Proxy: Caddy]    │
                 │     (Auto Let's Encrypt)    │
                 │              │              │
                 │              ▼              │
                 │     [AgentKit API] (:8000)  │
                 │      (FastAPI + Uvicorn)    │
                 │        │            │       │
                 │        ▼            ▼       │
                 │   [PostgreSQL]   [Redis]    │
                 │     (:5432)       (:6379)   │
                 │   (Internal)    (Internal)  │
                 └─────────────────────────────┘

The single-node EC2 architecture runs the complete containerized stack:

  • Reverse Proxy: Caddy or Nginx handling automatic TLS / HTTPS termination on ports 80/443.
  • AgentKit Application: FastAPI backend exposed on internal port 8000.
  • Database: PostgreSQL 16 Alpine container with attached persistent EBS volume.
  • Cache & Rate Limiting: Redis 7 Alpine container for session memory and rate limits.
  • Internal Network: Docker bridge network (agentkit_net) isolating PostgreSQL and Redis from public exposure.

2. EC2 Instance Sizing & Provisioning

Recommended Hardware Specs

  • Development / Staging: t3.small (2 vCPU, 2 GiB RAM)
  • Production Minimum: t3.medium (2 vCPU, 4 GiB RAM)
  • High Throughput: c6i.large or c7g.large (2 vCPU, 4 GiB RAM, Compute-optimized)
  • Root Volume: Minimum 30 GiB gp3 EBS volume (3000 IOPS, 125 MB/s throughput baseline).

Provisioning Steps (AWS CLI)

# Launch Ubuntu 24.04 LTS instance
aws ec2 run-instances \
    --image-id ami-04b70fa74e45c3917 \
    --instance-type t3.medium \
    --key-name your-ec2-keypair \
    --security-group-ids sg-0123456789abcdef0 \
    --subnet-id subnet-0123456789abcdef0 \
    --block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":30,"VolumeType":"gp3","DeleteOnTermination":false}}]' \
    --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=agentkit-production}]'

3. AWS Security Group Configuration

Caution

Never expose ports 5432 (PostgreSQL) or 6379 (Redis) to 0.0.0.0/0. They must remain accessible exclusively within the internal Docker network.

Protocol Port Source Description
SSH (TCP) 22 YOUR_OFFICE_IP/32 Administrative SSH access (strictly restricted)
HTTP (TCP) 80 0.0.0.0/0, ::/0 Let's Encrypt HTTP-01 challenge & HTTP -> HTTPS redirect
HTTPS (TCP) 443 0.0.0.0/0, ::/0 Encrypted public REST API traffic

AWS CLI Security Group Setup

# 1. Create dedicated Security Group
SG_ID=$(aws ec2 create-security-group \
    --group-name "agentkit-sg" \
    --description "Security Group for AgentKit EC2 Host" \
    --query "GroupId" --output text)

# 2. Allow SSH only from your trusted IP
aws ec2 authorize-security-group-ingress \
    --group-id "$SG_ID" \
    --protocol tcp --port 22 --cidr "203.0.113.45/32"

# 3. Allow public HTTP/HTTPS for Web & ACME TLS
aws ec2 authorize-security-group-ingress \
    --group-id "$SG_ID" \
    --protocol tcp --port 80 --cidr "0.0.0.0/0"
aws ec2 authorize-security-group-ingress \
    --group-id "$SG_ID" \
    --protocol tcp --port 443 --cidr "0.0.0.0/0"

4. Host Setup & Docker Installation

Connect to your EC2 instance via SSH:

ssh -i /path/to/key.pem ubuntu@<EC2_PUBLIC_IP>

Update system packages and install official Docker CE and Docker Compose:

# Update repository lists
sudo apt-get update && sudo apt-get upgrade -y
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# Install Docker GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Add Docker repository
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker Engine and Compose plugin
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Enable Docker daemon on boot and add ubuntu user to docker group
sudo systemctl enable --now docker
sudo usermod -aG docker ubuntu
newgrp docker

5. Storage & Volume Persistence

Create the application directory structure:

sudo mkdir -p /opt/agentkit /var/log/agentkit
sudo chown -R ubuntu:ubuntu /opt/agentkit /var/log/agentkit
cd /opt/agentkit

Clone the repository:

git clone https://github.com/getdownandcode/AgentKit.git .

Verify the local volume directories:

  • postgres_data: Docker managed volume or mount at /opt/agentkit/volumes/postgres
  • redis_data: Docker managed volume or mount at /opt/agentkit/volumes/redis

6. Environment & Secrets Management

Store secrets securely using AWS Systems Manager (SSM) Parameter Store or AWS Secrets Manager.

Option A: AWS SSM Parameter Store Integration

Fetch production secrets dynamically using the AWS CLI:

# Create parameters in AWS SSM
aws ssm put-parameter --name "/agentkit/production/DATABASE_PASSWORD" \
    --value "$(openssl rand -hex 24)" --type "SecureString"
aws ssm put-parameter --name "/agentkit/production/READONLY_PASSWORD" \
    --value "$(openssl rand -hex 24)" --type "SecureString"
aws ssm put-parameter --name "/agentkit/production/REDIS_PASSWORD" \
    --value "$(openssl rand -hex 24)" --type "SecureString"
aws ssm put-parameter --name "/agentkit/production/API_KEYS" \
    --value "prod_key_$(openssl rand -hex 16)" --type "SecureString"
aws ssm put-parameter --name "/agentkit/production/GEMINI_API_KEY" \
    --value "AIzaSy..." --type "SecureString"

# Export parameters directly into restricted .env file on the host
cat << 'EOF' > /opt/agentkit/fetch_secrets.sh
#!/usr/bin/env bash
set -euo pipefail

DB_PASS=$(aws ssm get-parameter --name "/agentkit/production/DATABASE_PASSWORD" --with-decryption --query "Parameter.Value" --output text)
RO_PASS=$(aws ssm get-parameter --name "/agentkit/production/READONLY_PASSWORD" --with-decryption --query "Parameter.Value" --output text)
REDIS_PASS=$(aws ssm get-parameter --name "/agentkit/production/REDIS_PASSWORD" --with-decryption --query "Parameter.Value" --output text)
API_KEYS=$(aws ssm get-parameter --name "/agentkit/production/API_KEYS" --with-decryption --query "Parameter.Value" --output text)
GEMINI_KEY=$(aws ssm get-parameter --name "/agentkit/production/GEMINI_API_KEY" --with-decryption --query "Parameter.Value" --output text)

# Credentials are written as components, not inside connection URLs: the generated
# passwords contain '/', '+' and '=', and AgentKit escapes them while it builds the URL.
cat << ENVFILE > /opt/agentkit/.env
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_USER=agentkit
POSTGRES_PASSWORD=${DB_PASS}
POSTGRES_READONLY_USER=agentkit_readonly
POSTGRES_READONLY_PASSWORD=${RO_PASS}
POSTGRES_DB=agentkit
REDIS_PASSWORD=${REDIS_PASS}
REDIS_URL=redis://redis:6379/0
LLM_PROVIDER=gemini
LLM_MODEL=gemini-3.1-flash-lite
GEMINI_API_KEY=${GEMINI_KEY}
API_KEYS=${API_KEYS}
FILE_TOOL_BASE_DIR=/tmp/agentkit_sandbox
RUN_TIMEOUT_S=60
LLM_MAX_RETRIES=3
ENVFILE

chmod 600 /opt/agentkit/.env
echo ".env successfully updated from AWS SSM."
EOF

chmod +x /opt/agentkit/fetch_secrets.sh
./opt/agentkit/fetch_secrets.sh

Option B: Manual Host-Level .env Configuration

If configuring manually, create /opt/agentkit/.env from .env.example:

cp .env.example .env
chmod 600 .env  # Restrict permissions: read/write only for owner

Datastore Exposure and Credentials

The compose stack treats PostgreSQL and Redis as internal services, because everything it stores — run goals, final answers, full conversation history, rate-limit counters — is readable by anyone who can reach those ports.

  • Loopback-only ports by default. postgres and redis publish on 127.0.0.1 (${POSTGRES_BIND:-127.0.0.1} / ${REDIS_BIND:-127.0.0.1}). Containers reach each other over agentkit-net; the host mapping exists only for local inspection. Setting POSTGRES_BIND=0.0.0.0 or REDIS_BIND=0.0.0.0 is a deliberate opt-in — do not do it on a shared host or CI runner without a firewall rule in front.
  • No default passwords. Compose requires POSTGRES_PASSWORD, POSTGRES_READONLY_PASSWORD and REDIS_PASSWORD; interpolation fails when one is missing instead of silently falling back to postgres/postgres or an unauthenticated Redis. Generate each one with openssl rand -base64 24.
  • Credentials are never interpolated into a URL. Compose passes POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD and the POSTGRES_READONLY_* pair; Settings assembles DATABASE_URL and DATABASE_READONLY_URL from them and percent-encodes every component. This matters because openssl rand -base64 24 produces /, + and =: a raw / ends the URL authority, so the remainder of the password becomes the path and the connection resolves to a database that does not exist. An explicitly configured DATABASE_URL / DATABASE_READONLY_URL still wins, for a managed database behind a secret substitution. Inside the stack the service names are authoritative, so a host-side .env pointing at localhost cannot leak into a container.
  • Redis authenticates. The service starts with redis-server --requirepass sourced from REDIS_PASSWORD. REDIS_PASSWORD is authoritative: it is embedded (percent-encoded) into a REDIS_URL that carries no credentials of its own, so the secret need not be repeated inside the URL, and an explicit password in the URL still wins. The healthcheck authenticates too, so a password mismatch surfaces as an unhealthy container rather than 401s at runtime. The real-infrastructure suite resolves credentials the same way, which is why AGENTKIT_REAL_INFRA=1 pytest -m real_infra tests/real works against this stack without a REDIS_TEST_URL — pass REDIS_PASSWORD (or let it come from .env) with any override URL.
  • The guard fails closed. The migrations service runs python scripts/check_deploy_credentials.py before anything else. It audits the credentials actually used, whether they come from components or from an explicit URL: it rejects the image defaults, the documented placeholders, short passwords, a URL with no password, a malformed URL, a missing read-only URL, a read-only role equal to the application role, and a REDIS_URL without a password. A URL-only deployment is not required to repeat the *_PASSWORD components it does not use.
  • Read-only SQL role. python scripts/init_readonly_role.py provisions POSTGRES_READONLY_USER with CONNECT/USAGE/SELECT only and default_transaction_read_only = on. The sql_readonly tool connects with it and refuses to run when DATABASE_READONLY_URL is absent, so the agent never touches the application role. A role that already exists is restricted, not merely given a new password: the script revokes SUPERUSER, CREATEDB, CREATEROLE, REPLICATION and BYPASSRLS, drops every inherited role membership (pg_read_server_files would otherwise allow pg_read_file) and revokes all non-SELECT object privileges before granting SELECT back. It then verifies the result and exits non-zero if anything remains, and it refuses a role that owns the database, because an owner can grant itself anything. At request time the tool independently refuses a superuser, a role with BYPASSRLS, or a role that is (directly or indirectly) a member of pg_read_server_files, pg_write_server_files, pg_execute_server_program, pg_signal_backend, pg_monitor or pg_maintain.

Verify the effective configuration before starting the stack:

docker compose config | grep -E "POSTGRES_BIND|REDIS_BIND|requirepass|POSTGRES_READONLY_USER|REDIS_URL"

No credential appears inside a URL in that output: the datastore URLs are built by the application from the component variables.


7. Systemd Service Daemon

Create a systemd service file at /etc/systemd/system/agentkit.service to manage the lifecycle of the Docker Compose stack across host restarts.

[Unit]
Description=AgentKit Docker Compose Application Service
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/agentkit
User=ubuntu
Group=docker

# Pre-execution: pull latest images or fetch credentials
ExecStartPre=-/usr/bin/docker compose pull --quiet
# Execution: start stack in detached mode
ExecStart=/usr/bin/docker compose up -d --remove-orphans
# Teardown: stop stack gracefully
ExecStop=/usr/bin/docker compose down --timeout 30

# Restart policy for the service unit itself
Restart=on-failure
RestartSec=10s
TimeoutStartSec=300

[Install]
WantedBy=multi-user.target

Reload systemd and enable the service on boot:

sudo systemctl daemon-reload
sudo systemctl enable agentkit.service
sudo systemctl start agentkit.service
sudo systemctl status agentkit.service

8. Reverse Proxy & Automatic SSL Termination

Option A: Caddy (Recommended - Zero Config Automated SSL)

Caddy automatically obtains and renews Let's Encrypt TLS certificates with no cron jobs required.

  1. Install Caddy on Ubuntu:
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLF 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLF 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update
sudo apt-get install -y caddy
  1. Configure /etc/caddy/Caddyfile:
api.yourdomain.com {
    # Automatic TLS enabled by default
    encode gzip zstd

    # Security Headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
    }

    # Proxy to AgentKit FastAPI backend
    reverse_proxy localhost:8000 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}

        # Healthcheck passive circuit breaker
        fail_duration 10s
        max_fails 3
    }

    # Access logging
    log {
        output file /var/log/caddy/agentkit_access.log {
            roll_size 50mb
            roll_keep 5
        }
    }
}
  1. Restart Caddy:
sudo systemctl restart caddy

Option B: Nginx + Certbot

If your organization standardizes on Nginx:

  1. Install Nginx and Certbot:
sudo apt-get install -y nginx certbot python3-certbot-nginx
  1. Create /etc/nginx/sites-available/agentkit:
server {
    server_name api.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket support (for streaming endpoints)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Timeouts for long agent reasoning runs
        proxy_connect_timeout 60s;
        proxy_send_timeout 120s;
        proxy_read_timeout 120s;
    }

    listen 80;
}
  1. Enable site and generate SSL certificate:
sudo ln -s /etc/nginx/sites-available/agentkit /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl restart nginx

# Obtain certificate via Let's Encrypt Certbot
sudo certbot --nginx -d api.yourdomain.com --non-interactive --agree-tos -m admin@yourdomain.com

9. Deployment & Verification Checklist

Once the stack is running, execute the verification checklist:

  1. Verify Docker Containers:

    docker compose ps
    # All 3 services (postgres, redis, api) should show 'healthy' status
  2. Verify Database Migrations:

    docker compose logs api | grep -i "running migrations"
  3. Verify API Healthcheck (Loopback):

    curl -s http://127.0.0.1:8000/health | jq .
    # Expected output:
    # {
    #   "status": "healthy",
    #   "database": "connected",
    #   "redis": "connected",
    #   "version": "0.1.0"
    # }
  4. Verify HTTPS & SSL Termination:

    curl -I https://api.yourdomain.com/health
    # Expected: HTTP/2 200 OK with valid TLS certificate
  5. Verify Authenticated Agent Run:

    curl -X POST https://api.yourdomain.com/runs \
      -H "X-API-Key: YOUR_CONFIGURED_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"goal": "Calculate 42 * 100 using calculator", "session_id": "test-session"}' | jq .

10. Monitoring, Backups & Maintenance

Automated PostgreSQL Backup Cron

Configure daily automated backups to AWS S3:

# Add backup script /opt/agentkit/backup_db.sh
cat << 'EOF' > /opt/agentkit/backup_db.sh
#!/usr/bin/env bash
set -euo pipefail
TIMESTAMP=$(date +\%Y\%m\%d_\%H\%M\%S)
BACKUP_FILE="/tmp/agentkit_backup_${TIMESTAMP}.sql.gz"

docker compose exec -T postgres pg_dump -U agentkit agentkit | gzip > "$BACKUP_FILE"
aws s3 cp "$BACKUP_FILE" "s3://your-agentkit-backups-bucket/db/${TIMESTAMP}.sql.gz"
rm -f "$BACKUP_FILE"
echo "Backup ${TIMESTAMP} completed successfully."
EOF

chmod +x /opt/agentkit/backup_db.sh

# Add to crontab: daily at 02:00 UTC
(crontab -l 2>/dev/null; echo "0 2 * * * /opt/agentkit/backup_db.sh >> /var/log/agentkit/backup.log 2>&1") | crontab -

Docker Log Management

To prevent containers from exhausting disk space, ensure Docker daemon log rotation is configured in /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "3"
  }
}

Apply with sudo systemctl restart docker.