Self-host Pantheon

Run the API, worker, migrations, and PostgreSQL with Docker Compose.

At a glance

API
localhost:8000
Services
Four
Database
PostgreSQL 15
Sandbox
Optional

Prerequisites

  • Install Docker Engine with Docker Compose.
  • Clone Pantheon and open the api directory.
  • Create an OpenRouter API key for model calls.
  • Create a Daytona API key only when an agent requires a sandbox.

Create the Compose file

Save this file as docker-compose.yaml in the api directory.

docker-compose.yaml
services:
  postgres:
    image: pgvector/pgvector:pg15
    environment:
      POSTGRES_USER: ai_user
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: pantheon_db
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ai_user -d pantheon_db"]
      interval: 2s
      timeout: 3s
      retries: 15

  migrate:
    build:
      context: .
      target: runtime
    image: pantheon:latest
    env_file: [.env]
    environment:
      POSTGRES_DATABASE_URL: postgresql+psycopg2://ai_user:secret@postgres:5432/pantheon_db
    depends_on:
      postgres:
        condition: service_healthy
    command: alembic upgrade head
    restart: "no"

  api:
    build:
      context: .
      target: runtime
    image: pantheon:latest
    env_file: [.env]
    environment:
      POSTGRES_DATABASE_URL: postgresql+psycopg2://ai_user:secret@postgres:5432/pantheon_db
    ports:
      - "8000:8000"
    depends_on:
      migrate:
        condition: service_completed_successfully
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000
    restart: unless-stopped

  worker:
    image: pantheon:latest
    env_file: [.env]
    environment:
      POSTGRES_DATABASE_URL: postgresql+psycopg2://ai_user:secret@postgres:5432/pantheon_db
    depends_on:
      migrate:
        condition: service_completed_successfully
    command: python -m app.modules.workers.runtime_worker
    restart: unless-stopped

volumes:
  pgdata:
ServicePurpose
postgresStores Pantheon data in the pgdata volume.
migrateApplies Alembic migrations before the API or worker starts.
apiServes the REST API on host port 8000.
workerClaims queued runs and executes agents.

Set the environment

Save the required values in api/.env. Restrict access to this file.

.env
OPENROUTER_API_KEY=replace-with-your-openrouter-secret
DAYTONA_API_KEY=
POSTGRES_DATABASE_URL=postgresql+psycopg2://ai_user:secret@postgres:5432/pantheon_db
PANTHON_BOOTSTRAP_ADMIN_KEY=ak_replace-with-a-long-random-secret
VariablePurposeSecret
OPENROUTER_API_KEYAuthorizes model calls.Yes
DAYTONA_API_KEYCreates sandbox environments. Leave it empty when requires_sandbox is false.Yes
POSTGRES_DATABASE_URLConnects each Pantheon service to PostgreSQL.Yes
PANTHON_BOOTSTRAP_ADMIN_KEYSeeds the first admin key when the API starts.Yes

Start Pantheon

1

Build and start

Compose waits for PostgreSQL, runs migrations, then starts the API and worker.

2

Check each service

The migration service should exit successfully. The API and worker should remain running.

3

Protect the admin key

The API stores its hash on first start. Keep the raw bootstrap value in a secret manager.

Terminal
cd /path/to/pantheon/api
docker compose up --build -d
docker compose ps
docker compose logs migrate api worker

Create the first tenant

Admin API

Use the bootstrap admin key to create a tenant and its first API key.

Terminal
export PANTHEON_BASE_URL="http://localhost:8000"
export PANTHEON_API_KEY="PANTHEON_API_KEY"

TENANT_JSON=$(curl -sS -X POST "$PANTHEON_BASE_URL/v1/admin/tenants" \
  -H "Authorization: Bearer $PANTHEON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Acme","slug":"acme"}')
TENANT_ID=$(printf '%s' "$TENANT_JSON" | jq -r '.id')

KEY_JSON=$(curl -sS -X POST "$PANTHEON_BASE_URL/v1/admin/tenants/$TENANT_ID/api-keys" \
  -H "Authorization: Bearer $PANTHEON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Development","permissions":["*"]}')
printf '%s' "$KEY_JSON" | jq '{id, tenant_id, name, permissions, key_preview, raw_key, created_at}'

The key creation response contains raw_key once.

Selected JSON
{
  "id": "key_123",
  "tenant_id": "tenant_123",
  "name": "Development",
  "permissions": ["*"],
  "key_preview": "pk_example...",
  "raw_key": "PANTHEON_API_KEY",
  "created_at": "2026-09-04T12:00:00Z"
}

Dashboard

After the auth release, open /dashboard/signup.html.

The dashboard talks to the API that served it, so no base URL setup is needed.

Sign up with an email, password, and organization name. Then create a key on the keys page.

Read Get an API key for the complete flow.

Upgrade

Back up PostgreSQL first. Then pull the source, rebuild images, migrate, and restart services.

Terminal
cd /path/to/pantheon/api
git pull --ff-only
docker compose build --pull
docker compose run --rm migrate
docker compose up -d --remove-orphans

Back up PostgreSQL

Create a compressed dump outside the database container.

Terminal
cd /path/to/pantheon/api
docker compose exec -T postgres \
  pg_dump -U ai_user -d pantheon_db -Fc > pantheon.dump

Store the dump away from the Docker host. Test restoration on a separate database.

Troubleshooting

SymptomCauseWhat to do
Migrations wait for a lock.Idle database connections still hold the migration lock.Stop the API and worker. Close idle connections, then run the migration again.
The API cannot bind port 8000.Another process uses the host port.Stop that process or change the left side of 8000:8000.
Sandbox creation fails.The Daytona account reached its quota.Increase the quota or use an agent with requires_sandbox set to false.

Pitfalls

  • The migration service keeps running: Inspect its logs before starting the API manually.
  • The worker cannot call a model: Set OPENROUTER_API_KEY in the shared .env file and restart the worker.
  • A sandbox request uses mock mode: Set DAYTONA_API_KEY before using an agent that requires a sandbox.