Docker Compose Stack
The Compose deployment in deploy/compose/: a Lite tier for trying it or single-user use, and a Full tier for self-hosting on one server. Both run the same container images that releases publish to GHCR. This is also the stack the nightly E2E workflow builds and tests.
Rules that apply here too
Treat
docker compose down on a shared or production stack as destructive: never run it unless someone explicitly asks for that exact command. To free ports, stop named services (
scripts/vegaduta compose stop <service>).
scripts/vegaduta down keeps volumes, but a stopped stack is still an outage.
- Files
deploy/compose/lite.yml (Lite), docker-compose.yml (Full, with profiles), lite.build.yml and docker-compose.build.yml (build from source), docker-compose.domain.yml (public HTTPS through Caddy), docker-compose.ci.yml and docker-compose.local-llm.yml (CI trims and a mock OpenAI-compatible provider)
- Config
- Untracked
deploy/compose/.env, generated by scripts/vegaduta secrets (random secrets, existing values never overwritten); the tracked template is deploy/compose/.env.example. Values are never reproduced in this documentation.
- CLI
scripts/vegaduta (vegaduta.ps1 on Windows): up, down, status, logs, doctor, secrets, upgrade, make-admin, compose
- Related
- Keycloak Provisioning · Local development · Service Topology
1. Tiers
| Tier | What you get | Containers | Docker memory | Command |
| Lite | Agents, chat, knowledge (optional RAG sidecar), graph memory | 5 (+1 with rag) | 6 GB | scripts/vegaduta up --lite / make up-lite |
| Full | Lite plus platform-admin console, S3 object storage, PgBouncer and opt-in profiles | 10 core, up to 36 with all profiles | 12 GB core set, 24 GB+ with all profiles | scripts/vegaduta up --full [--profile …] / make up PROFILE="temporal observability" |
2. Service groups
| Group | Services |
| Lite | postgres (PostgreSQL 16 with pgvector and Apache AGE), valkey, keycloak (dev-file store), core (profiles security,knowledge,graph-memory,lite, local-disk file storage), web; profile rag adds the sidecar |
| Full core set | postgres, pgbouncer, valkey, seaweedfs (S3 gateway), keycloak, keycloak-db, sidecar, core, web, web-admin |
Profile events | redpanda (Kafka API; BSL-1.1, see NOTICE) |
Profile temporal | temporal, temporal-postgresql, temporal-ui, core-worker (implies events) |
Profile jobs | core-jobs (implies temporal) |
Profile observability | prometheus, grafana, loki, promtail, alertmanager, valkey-exporter |
Profile tracing | Langfuse web and worker with their own postgres, clickhouse, valkey and S3 |
Profile voice | livekit, voice-agent |
| Other profiles | rl (rl-trainer), code-intel (with a private docker-in-docker executor, privileged), builder (app-builder, implies code-intel), sandbox (tool-sandbox, mcp-stdio-runner) |
Default local URLs: console http://localhost:3000, API http://localhost:8080, Keycloak http://localhost:8180, platform admin (Full) http://localhost:3001. Profiles need matching core settings (for example temporal sets WORKFLOW_EXECUTION_ENGINE=temporal); the CLI sets them, and with raw docker compose you set them in .env. Resource limits are set on every service and tuned with the *_MEM_LIMIT variables.
3. Domain overlay: what it changes
scripts/vegaduta up --full --domain example.com writes BASE_DOMAIN and the PUBLIC_* URLs into .env and adds docker-compose.domain.yml:
- Caddy obtains Let's Encrypt certificates for
app., admin., api. and auth. (plus codeintel., builder., livekit. when used) on your domain. The bare domain and www. are served too (they redirect to app.), so point their DNS at the server as well or ACME validation for those two names fails. Ports 80 and 443 must be open. Behind an existing TLS proxy or tunnel, set CADDY_AUTO_HTTPS=off and CADDY_SITE_SCHEME=http://, set PUBLIC_BIND_ADDR=127.0.0.1 so port 80 is not reachable around the tunnel, then forward to port 80 (see docs/operations/tunnel-hosting.md).
- Issuer consistency. Keycloak stamps tokens with
PUBLIC_AUTH_URL/realms/<realm>; core and the other services validate that exact issuer and fetch keys internally from http://keycloak:8080. Changing the domain later changes the issuer: add the old one to KEYCLOAK_ADDITIONAL_ISSUERS during the switch.
- Realm redirect URIs and web origins are written at the first import; after a domain change, update the
agentic-ai-web and agentic-ai-platform-admin clients in the Keycloak admin console (reachable on 127.0.0.1:8180 in domain mode).
4. Operating it
| Task | How |
| Check the environment | scripts/vegaduta doctor (docker/compose versions, RAM, disk, ports, secrets, URLs); make doctor |
| Status and logs | scripts/vegaduta status; scripts/vegaduta logs core |
| Build from source | scripts/vegaduta up --lite --build, or make images for the Full tier |
| Upgrade | scripts/vegaduta upgrade --version X.Y.Z; pin VEGADUTA_VERSION in production |
| Grant platform admin | scripts/vegaduta make-admin you@example.com |
| Raw compose | scripts/vegaduta compose <args> (right files and profiles) |
| Backups | See Backup & Restore; back up .env with the data |
| Validate the compose files | make compose-validate |
5. Known failure modes
| Symptom | Cause |
| Every API call returns 401 after login | Token issuer does not match what core expects. PUBLIC_AUTH_URL must be exactly the URL the browser uses for Keycloak; add other issuers to KEYCLOAK_ADDITIONAL_ISSUERS. |
Login redirects to localhost on a server | PUBLIC_* still localhost: re-run scripts/vegaduta secrets --domain example.com, then up. For images built with --build, rebuild web. |
| "Invalid redirect URI" | Realm clients were imported with other URLs; re-run scripts/vegaduta up, whose realm reconcile adds the derived URIs (it keeps entries you added by hand). |
| Signup succeeds but the console is empty or 500s | tenant_id attribute dropped: re-run the realm reconcile (scripts/vegaduta up or deploy/keycloak/reconcile-keycloak.sh). |
| Stored provider keys cannot be decrypted | APP_ENCRYPTION_KEY changed. Restore the original value; there is no other recovery. |
core unhealthy on first start | First boot runs all migrations; allow about 3 minutes, then read scripts/vegaduta logs core and check RAM with doctor. |
up fails with "port is already allocated" | Change *_HOST_PORT or BIND_ADDR in .env. |
| Workflows do not run | Enable --profile events (or temporal); the workflow Spring profile needs Kafka. |
| Voice connects but no audio | WebRTC media needs the UDP range LIVEKIT_RTC_PORT_RANGE_START-_END (default 50000-50100) reachable and LIVEKIT_BIND_ADDR=0.0.0.0; configure TURN for strict NATs. |
| Whole host crashes, all containers die | Host disk full. Build cache and old image layers accumulate; docker builder prune -f is safe for build cache only. |
Evidence & references
deploy/README.md — tiers, files, profiles, domain, operations, troubleshooting.
deploy/compose/lite.yml, docker-compose.yml, docker-compose.domain.yml, docker-compose.ci.yml, docker-compose.local-llm.yml, docker-compose.build.yml, lite.build.yml.
deploy/caddy/Caddyfile; Makefile; docs/installation.md.