This guide covers deploying Aegis in production environments. For development setup, see Getting Started.
- 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)
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
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:latestFor 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: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;
}
}
}# 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"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.targetInstall and enable:
sudo cp aegis.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable aegis
sudo systemctl start aegis| 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 |
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=trueKubernetes readiness probe:
readinessProbe:
httpGet:
path: /v1/health
port: 9100
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3Kubernetes liveness probe:
livenessProbe:
httpGet:
path: /v1/health
port: 9100
initialDelaySeconds: 15
periodSeconds: 20
failureThreshold: 3Aegis 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
}
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 ~/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"}| 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.
Server won't start:
# Check Claude Code is installed
claude --version
# Check port availability
ss -tlnp | grep 9100
# Run diagnostics
ag doctorSessions not starting:
# Check Claude Code is installed
claude --version
# Check ACP backend health
ag doctorReverse proxy returning 502:
# Ensure WebSocket headers are forwarded
proxy_set_header Connection "upgrade";