Deployment Stack and Example Configuration
- deploy/compose/docker-compose.yml
- deploy/compose/loki.yaml
- deploy/compose/prometheus.yaml
- deploy/compose/tempo.yaml
- korvus.example.yaml
Relevant source files · 11
- deploy/compose/loki.yaml 1–3, 10–30, 32–37
- deploy/compose/docker-compose.yml 8–16, 18–22, 47–54, 56–67 +1 more
- deploy/compose/docker-compose.test.yml 1–20, 24–47
- deploy/compose/prometheus.yml 5–14
- deploy/compose/tempo.yaml 4–11, 13–18, 20–26
- internal/config/config_test.go 475–504
- internal/config/config.go 270–290, 295–312, 324–367, 392–398
- korvus.example.yaml 1–13, 15–22, 24–31, 47–67 +4 more
- internal/mcp/connect.go 20–47
- internal/mcp/registry.go 60–78
- internal/telemetry/telemetry.go 46–92
Two artifacts describe how Korvus is actually run: korvus.example.yaml, a fully commented reference of the configuration surface, and the deploy/compose directory, a local stack that gives the agent something real to investigate. They belong together because the stack is the configuration's environment — the ports in mcp.servers, the otlp_endpoint and the database DSN all point at services the compose file starts. A developer who reads only one of them will get it wrong: the example file without the stack describes a deployment that does not exist, and the stack without the example file does not say how Korvus is told about any of it.
The stack itself is deliberately a development stack, not a production one. The Loki config says so in its first line — one replica, filesystem storage, no auth, no retention loki.yaml — and the same honesty applies to the rest: credentials are korvus/korvus/korvus, Grafana runs with anonymous admin access docker-compose.yml, and the whole thing binds ports on the developer's own machine.
What the compose stack starts
Six services, and the reason each one exists is more useful than the service list itself.
| Service | Image | Host port | Why it is there |
|---|---|---|---|
postgres |
pgvector/pgvector:pg17 |
5432 | The store, with the pgvector extension the embeddings column needs |
tempo |
grafana/tempo:3.1.0 |
4317, 4318, 3200 | Trace backend and OTLP receiver for Korvus's own spans |
prometheus |
prom/prometheus:latest |
9090 | Metrics backend the Prometheus MCP server reads |
loki |
grafana/loki:3.5.1 |
3100 | Log backend the Loki MCP server reads |
grafana |
grafana/grafana:latest |
3000 | A UI over the three backends |
checkout |
built from demo/checkout |
8090 | Synthetic traffic with a fault to inject |
Postgres is the pgvector/pgvector image rather than stock postgres on purpose: migration 001 runs CREATE EXTENSION vector and declares docs_chunks.embedding as vector(1536), so a stock image fails at migration time docker-compose.test.yml. The health check uses pg_isready against the korvus role, and a named volume keeps the data across restarts docker-compose.yml.
Tempo is the one image pinned to an exact patch release, and the comment above it explains why: Tempo 3 refuses to start on a 2.x config with field ingester not found, which is how a floating :latest tag once broke the whole stack docker-compose.yml. The pin keeps tempo.yaml and the image moving together — the config file was rewritten for Tempo 3 at the same time.
Loki gets the most careful health check in the file, because /ready reports unready for roughly thirty seconds while the ingester joins the ring; a dependent service that only waits for the port gets 503s on its first queries docker-compose.yml. The checkout service is the one that depends on it, with condition: service_healthy docker-compose.yml.
The checkout service maps host port 8090 to container port 8080 rather than the other way round, because Korvus itself wants 8080 in the dev loop docker-compose.yml. It is the demo toy service described in Checkout Demo Service and Telemetry: a checkout endpoint under steady load that emits real metrics, logs and traces, with a bad release deployable on demand. Its fault can be triggered automatically with FAULT_AFTER docker-compose.yml.
The three telemetry backends
Each backend has a small config file next to the compose file, mounted into its container.
Prometheus scrapes two jobs: itself, and the demo service at checkout:8080 prometheus.yml. The comment records the fault injection endpoint — POST http://localhost:8090/admin/deploy?version=1.4.1 — because that is how a reviewer reproduces an incident without waiting for a natural one.
Tempo is configured only for what the stack needs: an OTLP gRPC receiver on 4317 and an OTLP HTTP receiver on 4318 tempo.yaml, plus local filesystem storage for traces and the write-ahead log tempo.yaml. Retention lives under backend_worker.compaction.block_retention: 48h rather than the 2.x compactor block, because Tempo 3 has no ingester and compacts in the backend worker — a leftover ingester: key is a hard startup failure tempo.yaml.
Loki runs as a single binary with filesystem storage, an in-memory ring and a TSDB schema from 2024-01-01 loki.yaml. One limit deserves attention: max_entries_limit_per_query is left at its 5000 default because mcp-loki clamps requests to the same number, and lowering it here would make the two disagree silently loki.yaml. The ruler points at an Alertmanager that the dev stack does not run, which is harmless — the alert path in production is configured on the Korvus side instead, as described in Alert Ingestion, Fingerprinting and Debounce.
The example configuration
korvus.example.yaml is the operator-facing reference for every key in internal/config/config.go, and it is enforced rather than merely aspirational: the test TestExampleConfigLoads loads the example with KnownFields(true) and fails if the file and the struct have drifted apart config_test.go. The loader decodes with unknown fields rejected, so a typo is a startup error naming the key rather than a setting that was silently never read config.go.
The file's header states the substitution contract, and the loader implements exactly that. ${VAR} is replaced from the environment; a bare $VAR is left alone so a password containing a dollar sign survives; an unset variable is a startup error naming it, never an empty string korvus.example.yaml. The implementation collects every miss before returning, so a deployment missing five variables learns all five at once config.go. Commented-out lines are skipped during substitution, which is what makes the file's convention of documenting optional keys as commented-out ${VAR} lines safe config.go.
The sections map one-to-one onto the config structs:
| Section | What it controls | Notes |
|---|---|---|
server |
The single listen port | Connect, /healthz and the webhook share one mux behind h2c |
database |
The PostgreSQL DSN | max_conns must be at least 2 or migrations deadlock |
providers |
Completion models, with optional fallback |
openai_compatible or anthropic |
embeddings |
The embedding endpoint, separate from providers | Anthropic has no embeddings API |
workspace |
The git repo of skills, docs and templates | Synced with go-git, never the git binary |
blob |
Report and artifact bytes | path or endpoint, never both |
mcp |
The servers whose tools the agent may call | The only external I/O surface during a run |
telemetry |
OTLP export | Empty endpoint disables tracing |
session |
Scratch root and default template | |
incident |
Dedup window, ignored labels, debounce, close-on-resolved | |
skills |
Dev-only skill directory override | Only used when workspace.url is empty |
The server section is one port, not two korvus.example.yaml: the Connect handlers, the health check and the Alertmanager webhook all mount on one net/http mux served over h2c, so a single handler speaks gRPC, gRPC-Web and Connect. The same port discipline shows up in korvus CLI and Process Wiring.
The database section warns about a failure mode that looks like a bug but is a documented constraint: migrations pin one connection for the advisory lock and need a second for goose, so max_conns below 2 deadlocks at startup korvus.example.yaml. Validation enforces it and names the key config.go.
providers and embeddings are separate sections on purpose. Anthropic serves completions but has no embeddings endpoint, so a deployment can complete with one vendor and retrieve with another korvus.example.yaml. The dimensions setting is pinned to the width migration 001 fixed on docs_chunks.embedding; a mismatch is a startup error rather than an opaque insert failure at the first workspace sync, and the check reads the column of the database this process actually connects to. See PostgreSQL Persistence, Migrations and Vector Search.
blob accepts exactly one backend. Setting path and endpoint together is a startup error, because a session manager holds one blob store and naming two leaves no right answer about where the bytes went; setting neither is valid and reports still land in Postgres without their markdown korvus.example.yaml. The S3-compatible backend is not implemented yet — an endpoint is accepted and validated but stores nothing, and startup warns korvus.example.yaml. See Blob Storage Backend and Key Rules.
The mcp block deserves the closest reading, because its URLs must include the /mcp path. Each MCP server serves MCP at /mcp and a liveness probe at /healthz on the same port; a bare host:port fails the initialize handshake with "session not found" and the server is silently dropped from the catalog at startup korvus.example.yaml. Discovery is best-effort by design: an unreachable server degrades the skill catalog and is reported through the system status service, but it never fails startup connect.go, registry.go. The consequence for the skill catalog is described in MCP Client, Registry and Server Connection and Skill Catalog and Frontmatter Parsing.
telemetry.otlp_endpoint is the bridge from Korvus to Tempo in the dev stack, and it is where the ordering constraint in OpenTelemetry Setup and GenAI Semantic Attributes bites: an empty endpoint installs a no-op provider, and a populated one must be installed before anything instrumented runs telemetry.go.
The incident section is where the alert path's tunables live, and its ignored_labels default (pod, instance, uid) is explained rather than merely listed: those labels change on every restart, so including them would defeat deduplication entirely korvus.example.yaml. See Incident Manager: Open, List, Update, Close.
Finally, the file ends with a list of what is deliberately not configurable — the agent's iteration and time rails, notifications, and webhook authentication korvus.example.yaml. That is a design statement, not an omission.
The integration stack is a separate stack
deploy/compose/docker-compose.test.yml exists because integration tests need a database and nothing else. It deliberately does not start Tempo, Prometheus or Grafana, and it must not share a database with the development stack, since the suite applies migrations and writes to whatever the DSN points at — a dirty target fails with after up: got 7 of 8 tables, which reads like a broken migration rather than a stale schema docker-compose.test.yml.
It listens on 5433 rather than 5432, and it stores its data on a tmpfs mount so every run starts from a guaranteed-clean database and down cannot leave a stale volume for a later run to inherit docker-compose.test.yml. The file also sets an explicit project name korvus-test, because both compose files live in the same directory and would otherwise both default to the project name compose — the second up would adopt and reconfigure the first stack's containers docker-compose.test.yml.
CI mirrors this rather than consuming the dev stack. The root integration job declares its own pgvector/pgvector:pg17 service and points KORVUS_TEST_DATABASE_URL at it, and its rules deliberately exclude deploy/compose/docker-compose.yml on the grounds that nothing in CI consumes it, so a change there cannot affect any job's result.
Relationships
flowchart TD
compose["deploy/compose/docker-compose.yml"]
lokicfg["deploy/compose/loki.yaml"]
promcfg["deploy/compose/prometheus.yml"]
tempocfg["deploy/compose/tempo.yaml"]
testcompose["deploy/compose/docker-compose.test.yml"]
example["korvus.example.yaml"]
cfg["config.Load"]
validate["Config::Validate"]
connectall["ConnectAll"]
registry["Registry::RegisterServer"]
setup["telemetry.Setup"]
lokisrv["mcp-loki"]
promsrv["mcp-prometheus"]
temposrv["mcp-tempo"]
demo["demo-checkout"]
compose --> lokicfg
compose --> promcfg
compose --> tempocfg
compose --> demo
compose --> testcompose
example --> cfg
cfg --> validate
example --> connectall
connectall --> registry
lokisrv --> lokicfg
promsrv --> promcfg
temposrv --> tempocfg
example --> setup
setup --> tempocfg
demo --> lokicfg
demo --> tempocfgReference
| Symbol | Kind | What it does | Location |
|---|---|---|---|
docker-compose.yml |
Deployment | The development stack: Postgres, Tempo, Prometheus, Loki, Grafana, demo checkout | deploy/compose/docker-compose.yml |
docker-compose.test.yml |
Deployment | Ephemeral Postgres on 5433 for the integration suite | deploy/compose/docker-compose.test.yml |
loki.yaml |
Configuration | Single-binary Loki: filesystem storage, TSDB schema, no auth | deploy/compose/loki.yaml |
prometheus.yml |
Configuration | Scrape config for Prometheus itself and the checkout job |
deploy/compose/prometheus.yml |
tempo.yaml |
Configuration | Tempo 3: OTLP receivers, backend worker compaction, local storage | deploy/compose/tempo.yaml |
korvus.example.yaml |
Configuration | The operator-facing reference for every config key | korvus.example.yaml |
config.Load |
Function | Reads, substitutes ${VAR} and validates a config file |
internal/config/config.go |
config.Config::Validate |
Method | Reports every configuration problem at once, naming each key | internal/config/config.go |
mcp.ConnectAll |
Function | Dials each configured MCP server; failures degrade, never crash startup | internal/mcp/connect.go |
mcp.Registry::RegisterServer |
Method | Discovers a server's tools and enforces the global tool namespace | internal/mcp/registry.go |
telemetry.Setup |
Function | Installs the OTLP tracer provider, or a no-op when the endpoint is empty | internal/telemetry/telemetry.go |
run (mcp-loki) |
Function | Serves the Loki MCP tools over streamable HTTP on /mcp |
mcp-servers/loki/cmd/mcp-loki/main.go |
run (mcp-prometheus) |
Function | Serves the Prometheus MCP tools over streamable HTTP on /mcp |
mcp-servers/prometheus/cmd/mcp-prometheus/main.go |
run (mcp-tempo) |
Function | Serves the Tempo MCP tools over streamable HTTP on /mcp |
mcp-servers/tempo/cmd/mcp-tempo/main.go |
run (demo-checkout) |
Function | The demo toy service: load generator, fault injection, log and trace shipping | demo/checkout/cmd/demo-checkout/main.go |
Generated by astrolabe from commit 0c2dca73 on branch main.