# ────────────────────────────────────────────────────────────────────────────── # Steward — Production Docker Compose # # Services: # steward-api — REST API + vault (port 3200) # steward-proxy — Credential injection proxy (port 8080) # postgres — PostgreSQL 16 (internal only) # redis — Redis 7 for rate limiting + token store (internal only) # # Usage: # cp .env.example .env && $EDITOR .env # docker compose up -d # docker compose logs -f steward-api # # Override for dev: # docker compose -f docker-compose.yml -f docker-compose.dev.yml up # ────────────────────────────────────────────────────────────────────────────── services: # ── Steward API ────────────────────────────────────────────────────────────── steward-api: build: context: . dockerfile: Dockerfile image: steward:latest container_name: steward-api restart: unless-stopped depends_on: postgres: condition: service_healthy redis: condition: service_healthy environment: PORT: "${PORT:-3200}" STEWARD_BIND_HOST: "0.0.0.0" NODE_ENV: "production" DATABASE_URL: "${DATABASE_URL:-postgresql://${POSTGRES_USER:-steward}:${POSTGRES_PASSWORD:-changeme}@postgres:5432/${POSTGRES_DB:-steward}}" STEWARD_MASTER_PASSWORD: "${STEWARD_MASTER_PASSWORD:?STEWARD_MASTER_PASSWORD is required}" STEWARD_SESSION_SECRET: "${STEWARD_SESSION_SECRET:-}" STEWARD_PLATFORM_KEYS: "${STEWARD_PLATFORM_KEYS:-}" STEWARD_DEFAULT_TENANT_KEY: "${STEWARD_DEFAULT_TENANT_KEY:-}" AGENT_TOKEN_EXPIRY: "${AGENT_TOKEN_EXPIRY:-24h}" RPC_URL: "${RPC_URL:-https://mainnet.base.org}" CHAIN_ID: "${CHAIN_ID:-8453}" REDIS_URL: "redis://redis:6379" RESEND_API_KEY: "${RESEND_API_KEY:-}" EMAIL_FROM: "${EMAIL_FROM:-login@steward.fi}" APP_URL: "${APP_URL:-https://steward.fi}" PASSKEY_RP_NAME: "${PASSKEY_RP_NAME:-Steward}" PASSKEY_RP_ID: "${PASSKEY_RP_ID:-steward.fi}" PASSKEY_ORIGIN: "${PASSKEY_ORIGIN:-https://steward.fi}" GOOGLE_CLIENT_ID: "${GOOGLE_CLIENT_ID:-}" GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET:-}" DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}" DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}" ports: - "127.0.0.1:${PORT:-3200}:3200" healthcheck: test: ["CMD", "bun", "-e", "const r=await fetch('http://127.0.0.1:3200/ready');process.exit(r.ok?0:1)"] interval: 30s timeout: 10s start_period: 45s retries: 3 networks: - public - backend logging: driver: "json-file" options: max-size: "50m" max-file: "5" # ── Steward Proxy ──────────────────────────────────────────────────────────── steward-proxy: image: steward:latest container_name: steward-proxy restart: unless-stopped # Proxy only needs the API to be healthy (for JWT verification + DB access) depends_on: steward-api: condition: service_healthy command: ["bun", "run", "packages/proxy/src/index.ts"] environment: STEWARD_PROXY_PORT: "${STEWARD_PROXY_PORT:-8080}" NODE_ENV: "production" DATABASE_URL: "${DATABASE_URL:-postgresql://${POSTGRES_USER:-steward}:${POSTGRES_PASSWORD:-changeme}@postgres:5432/${POSTGRES_DB:-steward}}" STEWARD_MASTER_PASSWORD: "${STEWARD_MASTER_PASSWORD:?STEWARD_MASTER_PASSWORD is required}" STEWARD_SESSION_SECRET: "${STEWARD_SESSION_SECRET:-}" REDIS_URL: "redis://redis:6379" ports: - "127.0.0.1:${STEWARD_PROXY_PORT:-8080}:8080" healthcheck: test: ["CMD", "bun", "-e", "const r=await fetch('http://127.0.0.1:8080/health');process.exit(r.ok?0:1)"] interval: 30s timeout: 5s start_period: 15s retries: 3 networks: - public - backend logging: driver: "json-file" options: max-size: "50m" max-file: "5" # ── PostgreSQL 16 ───────────────────────────────────────────────────────────── postgres: image: postgres:16-alpine container_name: steward-postgres restart: unless-stopped environment: POSTGRES_DB: "${POSTGRES_DB:-steward}" POSTGRES_USER: "${POSTGRES_USER:-steward}" POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-changeme}" PGDATA: /var/lib/postgresql/data/pgdata volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-steward} -d ${POSTGRES_DB:-steward}"] interval: 10s timeout: 5s start_period: 15s retries: 5 networks: - backend logging: driver: "json-file" options: max-size: "20m" max-file: "3" # ── Redis 7 ─────────────────────────────────────────────────────────────────── redis: image: redis:7-alpine container_name: steward-redis restart: unless-stopped command: > redis-server --appendonly yes --appendfsync everysec --maxmemory 256mb --maxmemory-policy allkeys-lru volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s start_period: 10s retries: 5 networks: - backend logging: driver: "json-file" options: max-size: "20m" max-file: "3" # ── Volumes ─────────────────────────────────────────────────────────────────── volumes: postgres-data: driver: local redis-data: driver: local # ── Networks ────────────────────────────────────────────────────────────────── networks: # External-facing: steward-api and steward-proxy are reachable from outside public: driver: bridge # Internal-only: postgres and redis are not accessible from the host directly backend: driver: bridge internal: true