Self-hosted deployment
Run the full Sagewai stack on your own infrastructure. This guide covers local bring-up, environment configuration, database management, reverse proxy setup, fleet workers, and common failure modes.
How Sagewai is organized. Sagewai is always self-hosted open source (AGPL-3.0) — there is one product, which you run on your own infrastructure. The organization is the umbrella for your whole deployment. The project is the isolation boundary: project-scoped resources never cross project lines. A resource with
project_id = nullis org-global — shared across the whole org. In the defaultsingletenancy mode the org is a single trusted namespace and the project is an organizational filter; switch tomultito make the project a hard, enforced boundary. See Tenancy modes.
Current storage model. The
sagewai admin servebackend is a single-process, single-org server. Know these limits before deploying:
- Admin state (connections, providers, agents, profiles) is a JSON file (
~/.sagewai/admin-state.json). Mount it on a persistent volume; the container discards it on restart otherwise.- Fleet registry, task store, and harness store are in-memory and reset on every restart. Workers re-register on reconnect; in-flight tasks are lost if the process restarts.
- Sealed revocation and replay routes require
DATABASE_URL(orSAGEWAI_DATABASE_URL, which takes precedence) to be set. Without it those routes are simply not registered.- In the default single-org mode,
X-Project-IDis an organizational filter, not a security boundary. Any caller who can reach the API can supply any project ID. For hard isolation between projects or teams within your own deployment, enable multi-tenant mode — see Tenancy modes.- Provider secrets are encrypted at rest. Set
SAGEWAI_MASTER_KEYor mount a key file. Losing the key makes all stored secrets unrecoverable.Durable Postgres-backed stores for fleet, tasks, and harness are on the roadmap. Multi-tenant project isolation is available now via
SAGEWAI_TENANCY_MODE=multi(Postgres-backed, session-derived project scoping) — see Tenancy modes.
Tenancy modes
Sagewai runs in one of two tenancy modes, selected by SAGEWAI_TENANCY_MODE:
single(default) — one organization, one shared namespace for a trusted operator and team.X-Project-IDis a convenience filter for organizing resources, not a security boundary; any authenticated caller can pass any project ID. Admin state persists to a JSON file. This is the right mode for a self-hosted instance run by a single team that shares access to everything.multi— one organization with many isolated projects (the tenant unit), for keeping projects or teams within your own deployment separated. Project scope is a hard, session-derived boundary: a request only ever reaches its own project's resources plus org-shared (global) ones; a forgedX-Project-IDis rejected (404). Requires a PostgreSQLDATABASE_URL. Use this when you need hard per-project isolation inside your org — for example an agency isolating each client's work, or an enterprise isolating different teams from one another.
In both modes, provider secrets are encrypted at rest, host-backed execution is denied by default, and the admin API is auth-gated (CORS allowlist, HttpOnly+CSRF cookies). Multi-tenant mode additionally enforces per-project key envelopes, per-project run quotas, and a hash-chained per-project audit log.
Rate limiting is automatically distributed in multi-tenant mode. The login
brute-force lockout and the per-project run-rate quota (SAGEWAI_PROJECT_RUN_RATE /
SAGEWAI_PROJECT_RUN_WINDOW) are enforced across all worker processes via the
same PostgreSQL database the tenant stores use — no Redis or extra configuration
required, so the limits hold correctly even with multiple replicas behind a load
balancer. Single-org mode uses an equivalent in-process limiter (no database
dependency), which is exact for a single worker.
Feature support matrix
| Surface | Status |
|---|---|
| Providers, agents, connections, runs, prompt logs | Supported (project-isolated in multi) |
| Vector & graph memory, context | Supported — in-memory by default; durable via Milvus / NebulaGraph |
| MCP tool execution | Supported for remote HTTP/SSE servers; stdio requires SAGEWAI_ALLOW_HOST_EXEC=1 |
| Fleet workers | Supported — registry is in-memory and resets on restart |
| Harness (LLM gateway) | Supported |
| Budget, guardrails, notifications, triggers, eval, artifact destinations | Supported (file-backed; single-process) |
Preview / not yet supported
A few surfaces are exposed in the API or admin UI but are preview-only or not yet implemented in this build. They are listed here so the deployment is honest about what does and does not work end-to-end:
| Surface | Status |
|---|---|
| Sealed Modes 2 / 3 / 3b (identity / full / full + JIT execution) | Preview — off by default. These inject per-workload credentials and depend on Sealed runtime enforcement that is not wired into the default worker path. Refused in multi mode unless you opt in with SAGEWAI_SEALED_PREVIEW=1. Modes 0/1 (bare / managed) are fully supported. |
Legacy workflow dispatch (POST /workflows/dispatch) | Not implemented (returns 501). Enqueue workflows from the builder or via POST /api/v1/workflows/enqueue. The admin Dispatch page is inert. |
Prompt replay (POST /api/v1/prompts/replay) | Not available standalone (returns 501) — replay requires a running agent. Re-running a workflow from its history page (which re-submits the definition) works. |
Eval run (POST /api/v1/eval/run) | Not available (returns 501) — requires a running agent with LLM keys. Datasets and reports are otherwise managed normally. |
| Run pause / resume | Not implemented — there is no backend route. Only cancel is supported for an in-flight run. |
Prerequisites
Before you start, install:
- Docker and Docker Compose v2+
- PostgreSQL 14+ (or use the provided container)
- 4 GB RAM minimum for a local deployment; 8 GB or more for production
- A domain name and TLS certificate for production deployments
- At least one LLM API key (OpenAI, Anthropic, or Google)
Quick start
git clone https://github.com/sagewai/platform.git
cd platform
cp .env.example .env
# Fill in your API keys in .env
make start # infra + DB migrations + admin backend
make web APP=admin # admin frontend (run in a second terminal)
make start brings up PostgreSQL, Redis, Milvus, NebulaGraph, and the observability stack, applies database migrations, and starts the admin backend. The frontend runs separately.
Once up:
- Admin API at
http://localhost:8000 - Admin UI at
http://localhost:3008(aftermake web) - Grafana at
http://localhost:3200
Architecture overview
| Service | Purpose | Port |
|---|---|---|
| PostgreSQL 14+ | State, workflows, fleet, audit | 5432 |
| Redis 7 | Cache, sessions | 6379 |
| Milvus 2.3 | Vector embeddings for RAG | 19530 |
| NebulaGraph 3.6 | Knowledge graph, relations | 9669 |
| Grafana | Dashboards, alerting | 3200 |
| Prometheus | Metrics scraping | 9090 |
| OTel Collector | Distributed tracing | 4317 |
| LocalStack | S3-compatible archive storage | 4566 |
Docker Compose profiles
Sagewai ships two compose files.
Full infrastructure (docker-compose.yml)
Starts all infrastructure and observability services:
make infra # Postgres, Redis, Milvus, NebulaGraph, Grafana, Prometheus, OTel
make infra-core # Postgres + Redis only
Development stack (docker-compose.dev.yml)
Extends the infrastructure compose with dockerized backends (admin, nexus, chronicles, haus) and a db-migrate init container:
make start # Infrastructure + migrations + admin backend + frontend
make down # Stop everything
Production overrides
Create a docker-compose.prod.yml overlay for production resource limits:
# docker-compose.prod.yml
services:
postgres:
deploy:
resources:
limits:
memory: 2G
restart: always
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
redis:
restart: always
deploy:
resources:
limits:
memory: 512M
milvus:
restart: always
deploy:
resources:
limits:
memory: 4G
Start with both files:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Environment variables
Copy .env.example and fill in the values you need.
Required
| Variable | Description | Default |
|---|---|---|
DATABASE_URL | PostgreSQL connection string (also accepted as SAGEWAI_DATABASE_URL, which takes precedence). Enables Sealed revocation and replay routes. | postgresql://sagewai:sagewai_password@localhost:5432/sagewai |
OPENAI_API_KEY | OpenAI API key (or another provider key) | — |
JWT_SECRET | Secret for JWT token signing | — |
Infrastructure
| Variable | Description | Default |
|---|---|---|
REDIS_URL | Redis connection URL | redis://localhost:6379 |
MILVUS_HOST | Milvus vector DB host | localhost |
MILVUS_PORT | Milvus vector DB port | 19530 |
MILVUS_URI | Milvus URI (for Zilliz Cloud) | — |
MILVUS_TOKEN | Milvus auth token (for Zilliz Cloud) | — |
NEBULA_HOST | NebulaGraph host | localhost |
NEBULA_PORT | NebulaGraph port | 9669 |
NEBULA_USER | NebulaGraph username | root |
NEBULA_PASSWORD | NebulaGraph password | nebula |
AI providers
| Variable | Description | Required |
|---|---|---|
OPENAI_API_KEY | OpenAI API key | At least one |
ANTHROPIC_API_KEY | Anthropic API key | Optional |
GOOGLE_API_KEY | Google AI API key | Optional |
LITELLM_PROXY_URL | LiteLLM proxy URL | Optional |
LITELLM_API_KEY | LiteLLM master key | Optional |
Security
| Variable | Description | Notes |
|---|---|---|
JWT_SECRET | JWT signing secret | Required for auth |
JWT_ALGORITHM | JWT algorithm | Default: HS256 |
SAGEWAI_ENCRYPTION_KEY | Fernet key for encrypting stored secrets | Recommended |
SAGEWAI_ALLOW_HOST_EXEC | Enable host-backed execution (on-host NullBackend / bash / stdio MCP) | Disabled by default everywhere. Set to 1 to enable — required for local self-hosted autopilot or any workflow that runs code directly on the host. Do not set in Internet-facing containers. |
SAGEWAI_TENANCY_MODE | single (default) or multi — see Tenancy modes | multi enables hard per-project isolation and requires a PostgreSQL DATABASE_URL. |
SAGEWAI_ADMIN_TLS | Marks the admin deployment as TLS-terminated | Set to 1 in production. Without it, session cookies are issued without the Secure flag and HSTS is not emitted. Set it when serving over HTTPS (directly or behind a TLS-terminating reverse proxy). |
Observability
| Variable | Description | Default |
|---|---|---|
LOG_LEVEL | Logging level | INFO |
LOG_FORMAT | Log format (json or text) | json |
OTEL_EXPORTER_OTLP_ENDPOINT | OpenTelemetry collector endpoint | http://localhost:4317 |
Database setup
Initial setup
Apply migrations after starting PostgreSQL:
make db-upgrade # Apply all Alembic migrations
make db-seed # Seed demo data (optional)
Or do both together:
make db-fresh # Drop schema + migrate + seed
Backup and restore
# Backup
pg_dump -h localhost -U sagewai sagewai > backup.sql
# Restore
psql -h localhost -U sagewai sagewai < backup.sql
Migration management
make db-upgrade # Apply pending migrations
make db-reset # Drop and recreate schema (destructive)
make db-fresh # Reset + migrate + seed
Caution:
db-resetdrops the entire schema. Back up first on any system that holds real data.
Reverse proxy configuration
Place a reverse proxy in front of Sagewai in production to terminate TLS.
Nginx
server {
listen 443 ssl http2;
server_name sagewai.example.com;
ssl_certificate /etc/nginx/certs/cert.pem;
ssl_certificate_key /etc/nginx/certs/key.pem;
# Admin frontend
location / {
proxy_pass http://localhost:3008;
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;
}
# Admin API + Fleet gateway
location /api/ {
proxy_pass http://localhost: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 (AG-UI, live updates)
location /ws/ {
proxy_pass http://localhost:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400;
}
# SSE streams (notifications, workflow events)
location /api/v1/notifications/stream {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400;
}
}
Caddy (automatic TLS)
Caddy handles Let's Encrypt certificate issuance and renewal automatically:
sagewai.example.com {
handle /api/* {
reverse_proxy localhost:8000
}
handle /ws/* {
reverse_proxy localhost:8000
}
handle {
reverse_proxy localhost:3008
}
}
TLS / HTTPS
Three options:
- Let's Encrypt via Caddy — automatic certificate issuance and renewal, no manual configuration.
- Let's Encrypt via certbot — use with nginx; configure a renewal cron job.
- Organizational CA — for internal or enterprise deployments behind a corporate proxy.
For internal deployments with a self-signed certificate:
openssl req -x509 -nodes -days 365 \
-newkey rsa:2048 \
-keyout /etc/nginx/certs/key.pem \
-out /etc/nginx/certs/cert.pem \
-subj "/CN=sagewai.internal"
Fleet workers
Fleet workers connect to the central gateway and run workflows on your own hardware with your own LLM API keys.
Docker
docker run -d \
-e FLEET_GATEWAY_URL=https://sagewai.example.com \
-e ENROLLMENT_KEY=ek-your-enrollment-key-here \
-e WORKER_POOL=default \
-e OPENAI_API_KEY=sk-... \
sagewai/worker:latest
Note: The
sagewai/workerimage is not yet published to a registry. Build it locally first:docker build -t sagewai/worker:latest infra/docker/worker/
Docker Compose
Copy the example file from infra/docker/worker/docker-compose.example.yml:
cd infra/docker/worker
cp docker-compose.example.yml docker-compose.yml
# Set FLEET_GATEWAY_URL, ENROLLMENT_KEY, and API keys
# Start one worker
docker compose up -d
# Scale to 3 workers
docker compose up -d --scale worker=3
With local models (Ollama)
Workers can advertise locally available models to the gateway:
docker run -d \
-e FLEET_GATEWAY_URL=https://sagewai.example.com \
-e ENROLLMENT_KEY=ek-your-enrollment-key-here \
-e WORKER_POOL=gpu-workers \
-e WORKER_MODELS=llama3,mistral \
-e WORKER_LABELS="gpu=true,env=production" \
sagewai/worker:latest
Worker enrollment flow
- Generate an enrollment key in the admin panel at Fleet > Enrollment Keys > New Key, or run
sagewai fleet create-key. - Start the worker with the enrollment key set as
ENROLLMENT_KEY. - Approve the worker in the admin panel at Fleet > Workers.
- The worker begins receiving tasks immediately after approval.
Monitoring
After running make infra, the following observability tools are available:
| Tool | URL | Purpose |
|---|---|---|
| Grafana | http://localhost:3200 | Dashboards for agent metrics, costs, latency |
| Prometheus | http://localhost:9090 | Metrics scraping and alerting |
| OTel Collector | :4317 (gRPC) | Distributed tracing (Jaeger/Zipkin-compatible) |
The admin panel also provides built-in health monitoring at Settings > Infrastructure.
Health checks
Verify your deployment with:
# Admin API health summary
curl http://localhost:8000/api/v1/health/summary
# Detailed component status (Postgres, Redis, Milvus, NebulaGraph)
curl http://localhost:8000/api/v1/health/detailed
Both endpoints return JSON with per-component status.
Upgrading
# Pull latest code
git pull origin main
# Apply any new migrations
make db-upgrade
# Restart all services
make start
If you run images directly:
docker compose pull
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
make db-upgrade
Back up your database before applying migrations on a production system.
Running without optional services
Sagewai degrades when optional infrastructure is unavailable:
| Service | If missing | Fallback |
|---|---|---|
| Milvus | Vector search disabled | In-memory embeddings (development only) |
| NebulaGraph | Graph memory disabled | In-memory graph store |
| Redis | No caching | Direct DB queries |
| LocalStack | No S3 archival | Local filesystem archives |
The minimum viable deployment requires PostgreSQL only. Start with:
make infra-core # Postgres + Redis
make db-upgrade
make dev-native APP=admin
Troubleshooting
Milvus fails to start
Milvus depends on etcd and MinIO. If it fails with connection errors, check both dependencies:
docker compose logs etcd
docker compose logs minio
docker compose logs milvus
Milvus can take up to 90 seconds to become healthy on first startup. The start_period in the healthcheck configuration controls this window.
NebulaGraph connection refused
NebulaGraph has three components (metad, storaged, graphd) that start in a specific order. Wait until all three show healthy before connecting:
docker compose ps | grep nebula
Database migration errors
If migrations fail after pulling new code:
make db-reset # Drop and recreate schema
make db-upgrade # Apply all migrations from scratch
make db-seed # Re-seed demo data if needed
Out of memory
Milvus uses the most memory of any component. For memory-constrained environments:
- Use
make infra-coreinstead ofmake infra— this skips Milvus, NebulaGraph, and the observability stack. - Set memory limits in a production compose override.
- Consider using Zilliz Cloud (managed Milvus) to offload vector storage.
Port conflicts
Default ports used by Sagewai:
| Port | Service |
|---|---|
| 3008 | Admin frontend |
| 3200 | Grafana |
| 4317 | OTel Collector |
| 4566 | LocalStack (S3) |
| 5432 | PostgreSQL |
| 6379 | Redis |
| 8000 | Admin backend |
| 9090 | Prometheus |
| 9669 | NebulaGraph |
| 19530 | Milvus |
If any port is already in use, stop the conflicting service or change the port mapping in docker-compose.yml.