Skip to content

Latest commit

Β 

History

History
446 lines (348 loc) Β· 10.3 KB

File metadata and controls

446 lines (348 loc) Β· 10.3 KB

Production Deployment Guide

This guide covers deploying Aegis in production environments. For development setup, see Getting Started.


Prerequisites

  • Node.js 20+ (LTS recommended)
  • Claude Code CLI installed and configured (claude --version)
  • Docker (optional, for containerized deployment)
  • nginx (optional, for reverse proxy + TLS termination)

Docker

Dockerfile

Aegis runs as a minimal Node.js container. Use multi-stage builds to keep the image small:

# syntax=docker/dockerfile:1
FROM node:20-slim AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:20-slim

# Install Claude Code dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Create non-root user for security
RUN groupadd -r aegis && useradd -r -g aegis aegis

WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./package.json

# Copy Claude Code binary if bundled
COPY --chown=aegis:aegis . /app

USER aegis
EXPOSE 9100

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:9100/v1/health || exit 1

ENTRYPOINT ["node", "dist/server.js"]

Build and Run

# Build
docker build -t aegis:latest .

# Run
docker run -d \
  --name aegis \
  -p 9100:9100 \
  -v ~/.aegis:/home/aegis/.aegis \
  -e AEGIS_AUTH_TOKEN=your-secret-token \
  -e AEGIS_HOST=0.0.0.0 \
  --restart unless-stopped \
  aegis:latest

Docker Compose (Full Stack)

For a complete production stack with nginx reverse proxy:

version: '3.8'

services:
  aegis:
    build: .
    container_name: aegis
    restart: unless-stopped
    ports:
      - "127.0.0.1:9100:9100"
    volumes:
      - aegis-data:/home/aegis/.aegis
    environment:
      - AEGIS_AUTH_TOKEN=${AEGIS_AUTH_TOKEN}
      - AEGIS_HOST=127.0.0.1
      - AEGIS_PORT=9100
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9100/v1/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  nginx:
    image: nginx:alpine
    container_name: aegis-proxy
    restart: unless-stopped
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      aegis:
        condition: service_healthy

volumes:
  aegis-data:

Nginx Reverse Proxy + TLS

nginx.conf

worker_processes auto;
worker_rlimit_nofile 65536;

events {
    worker_connections 4096;
    use epoll;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # Security headers
    add_header X-Frame-Options        "SAMEORIGIN"     always;
    add_header X-Content-Type-Options "nosniff"        always;
    add_header X-XSS-Protection       "1; mode=block"  always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # Logging
    access_log /var/log/nginx/aegis.access.log combined;
    error_log  /var/log/nginx/aegis.error.log warn;

    # Performance
    sendfile           on;
    tcp_nopush         on;
    tcp_nodelay        on;
    keepalive_timeout  65;
    gzip               on;
    gzip_types         text/plain application/json application/javascript;

    # Rate limiting zones
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;

    upstream aegis_backend {
        server 127.0.0.1:9100;
        keepalive 32;
    }

    server {
        listen 80;
        server_name aegis.yourdomain.com;

        # Redirect HTTP to HTTPS
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name aegis.yourdomain.com;

        # TLS configuration
        ssl_certificate     /etc/nginx/certs/aegis.crt;
        ssl_certificate_key /etc/nginx/certs/aegis.key;
        ssl_protocols       TLSv1.2 TLSv1.3;
        ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
        ssl_prefer_server_ciphers off;
        ssl_session_cache   shared:SSL:10m;
        ssl_session_timeout 1d;

        # Security
        allow 10.0.0.0/8;
        allow 172.16.0.0/12;
        allow 192.168.0.0/16;
        deny all;

        # Proxy to Aegis
        location / {
            proxy_pass         http://aegis_backend;
            proxy_http_version 1.1;
            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;
            proxy_set_header   Connection         "";

            # Timeouts
            proxy_connect_timeout 10s;
            proxy_send_timeout    60s;
            proxy_read_timeout    60s;

            # SSE / WebSocket support
            proxy_buffering        off;
            proxy_cache           off;
        }

        # SSE endpoint β€” long-lived connections
        location /v1/events {
            proxy_pass         http://aegis_backend;
            proxy_http_version 1.1;
            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;
            proxy_set_header   Connection      upgrade;
            proxy_buffering    off;
            proxy_cache       off;
            proxy_read_timeout 86400s;
            chunked_transfer_encoding on;
        }
    }
}

Obtaining TLS Certificates

# Using Let's Encrypt + Certbot
sudo certbot --nginx -d aegis.yourdomain.com

# Or manual with openssl (self-signed for testing)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
    -keyout certs/aegis.key -out certs/aegis.crt \
    -subj "/CN=aegis.yourdomain.com"

Systemd Service

For bare-metal or VM deployments:

[Unit]
Description=Aegis Session Orchestrator
After=network.target
Wants=network.target

[Service]
Type=simple
User=aegis
Group=aegis
WorkingDirectory=/home/aegis
ExecStart=/usr/local/bin/node /home/aegis/dist/server.js
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

# Environment
Environment=NODE_ENV=production
Environment=AEGIS_AUTH_TOKEN=change-me-in-production
Environment=AEGIS_HOST=127.0.0.1
Environment=AEGIS_PORT=9100

# Security: restrict syscalls
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/home/aegis/.aegis /tmp

[Install]
WantedBy=multi-user.target

Install and enable:

sudo cp aegis.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable aegis
sudo systemctl start aegis

Production Environment Variables

Variable Default Description
AEGIS_HOST 127.0.0.1 Bind address (use 0.0.0.0 behind reverse proxy)
AEGIS_PORT 9100 HTTP server port
AEGIS_AUTH_TOKEN β€” Required. Secret token for API authentication
AEGIS_STATE_DIR ~/.aegis Directory for sessions, transcripts, config
AEGIS_ALLOWED_WORK_DIRS ["~"] Restrict session working directories
AEGIS_ACP_BIN claude-agent-acp Path to the ACP binary (auto-detected)
AEGIS_SESSION_TTL_MS 3600000 Session auto-cleanup after inactivity
AEGIS_MAX_SESSIONS 50 Hard limit on concurrent sessions
AEGIS_HOOK_SECRET_HEADER_ONLY false Enforce header-only hook secrets

Health Checks and Readiness

Aegis exposes two health endpoints:

# Liveness probe β€” is the server process alive?
curl http://localhost:9100/v1/health

# Readiness probe β€” is the server ready to accept traffic?
curl http://localhost:9100/v1/health?ready=true

Kubernetes readiness probe:

readinessProbe:
  httpGet:
    path: /v1/health
    port: 9100
  initialDelaySeconds: 5
  periodSeconds: 10
  failureThreshold: 3

Kubernetes liveness probe:

livenessProbe:
  httpGet:
    path: /v1/health
    port: 9100
  initialDelaySeconds: 15
  periodSeconds: 20
  failureThreshold: 3

Log Management

Aegis writes structured JSON logs to stdout. Route them to your log aggregator:

# JSON log format example
{"level":"info","time":"2026-04-22T10:00:00.000Z","msg":"Server started","host":"0.0.0.0","port":9100}

# Ship to stdout (Docker/Kubernetes) β€” collect with fluentd/fluent-bit
# Ship to file
node dist/server.js 2>&1 | tee /var/log/aegis.log

# Rotate logs
sudo logrotate -f /etc/logrotate.d/aegis

/etc/logrotate.d/aegis:

/var/log/aegis.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    postrotate
        systemctl reload aegis > /dev/null 2>&1 || true
    endscript
}

Backup and Recovery

Session Data

Aegis stores all state in AEGIS_STATE_DIR (default ~/.aegis):

# Backup
tar -czf aegis-backup-$(date +%Y%m%d).tar.gz \
    ~/.aegis/sessions/ \
    ~/.aegis/transcripts/ \
    ~/.aegis/config.json

# Restore
tar -xzf aegis-backup-YYYYMMDD.tar.gz -C ~/

Session Transcript Format

Each session has a JSONL transcript at ~/.aegis/transcripts/{session-id}.jsonl. Each line is a message object:

{"role":"user","contentType":"text","text":"Hello","timestamp":"2026-04-22T10:00:00.000Z"}
{"role":"assistant","contentType":"text","text":"Hi, how can I help?","timestamp":"2026-04-22T10:00:01.000Z"}

Capacity Planning

Concurrent Sessions RAM (approx.) CPU
10 512 MB 1 core
50 2 GB 2 cores
100 4 GB 4 cores
500 16 GB 8 cores

Each Claude Code session uses ~50-200 MB RAM depending on context size.


Troubleshooting

Server won't start:

# Check Claude Code is installed
claude --version

# Check port availability
ss -tlnp | grep 9100

# Run diagnostics
ag doctor

Sessions not starting:

# Check Claude Code is installed
claude --version

# Check ACP backend health
ag doctor

Reverse proxy returning 502:

# Ensure WebSocket headers are forwarded
proxy_set_header Connection "upgrade";