Configuration
Docker#
The container has no access to your agents unless you mount them. That is the main thing to get right.
Build and run#
docker build -t clawmetry .
docker run -d --name clawmetry \
-p 8900:8900 \
-v clawmetry-state:/root/.clawmetry \
-v ~/.openclaw:/root/.openclaw:ro \
-v ~/.claude:/root/.claude:ro \
-v ~/.codex:/root/.codex:ro \
clawmetryThree rules:
- Mount runtime directories
:ro. ClawMetry never writes to them, and the
mount flag makes that structural rather than a promise.
- Persist
/root/.clawmetryon a named volume. Without it the store is
discarded on every recreate, and you re-ingest from scratch each time.
- Mount every runtime you want observed. Detection is a filesystem check;
an unmounted runtime does not exist as far as the container is concerned.
Compose#
services:
clawmetry:
build: .
ports:
- "127.0.0.1:8900:8900"
volumes:
- clawmetry-state:/root/.clawmetry
- ${HOME}/.openclaw:/root/.openclaw:ro
- ${HOME}/.claude:/root/.claude:ro
- ${HOME}/.codex:/root/.codex:ro
- ${HOME}/.cursor:/root/.cursor:ro
environment:
CLAWMETRY_HOME: /root/.clawmetry
CLAWMETRY_FAMILY_SESSION_LIMIT: "100"
CLAWMETRY_NO_BROWSER: "1"
restart: unless-stopped
volumes:
clawmetry-state:Note 127.0.0.1:8900:8900 rather than 8900:8900 — the second form publishes
the dashboard on every interface, and the dashboard shows your agents'
transcripts. → Ports and networking
Splitting daemon and dashboard#
For anything beyond a single-machine setup, run them as two services sharing the state volume. Only the daemon writes.
services:
sync:
build: .
command: python -m clawmetry.sync
volumes:
- clawmetry-state:/root/.clawmetry
- ${HOME}/.openclaw:/root/.openclaw:ro
restart: unless-stopped
dashboard:
build: .
command: clawmetry --host 0.0.0.0 --port 8900 --no-debug
depends_on: [sync]
ports:
- "127.0.0.1:8900:8900"
volumes:
- clawmetry-state:/root/.clawmetry
restart: unless-stopped
volumes:
clawmetry-state:--host 0.0.0.0 is correct inside the container — the port publishing is what
restricts exposure. The dashboard finds the daemon through the shared
local_query.json, which is why the state volume must be shared and not just
similar.
Paths inside the container#
Runtimes resolve paths relative to the container's home directory, so mount to
the path the runtime expects. If you run as a non-root user, mount to that
user's home instead of /root, or set the override:
environment:
OPENCLAW_HOME: /data/openclaw
CLAUDE_CONFIG_DIR: /data/claudeCloud sync from a container#
environment:
CLAWMETRY_API_KEY: ${CLAWMETRY_KEY}
CLAWMETRY_NODE_ID: prod-runner-01Set CLAWMETRY_NODE_ID explicitly. A container's hostname is a random hex
string that changes on every recreate, which produces a fleet list of
one-session ghosts.
Pass the encryption key too, so a recreated container keeps reading its own history:
environment:
CLAWMETRY_ENC_KEY: ${CLAWMETRY_ENC_KEY}Observing agents in other containers#
If your agents run in their own containers, share a volume between the agent and ClawMetry:
services:
my-agent:
image: my-agent
volumes:
- agent-state:/root/.openclaw
clawmetry:
build: .
volumes:
- agent-state:/root/.openclaw:ro
- clawmetry-state:/root/.clawmetryFor agents you cannot co-locate, use OTLP or the ingest API instead of sharing a filesystem.
Health#
curl -s localhost:8900/healthz
docker exec clawmetry clawmetry statushealthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8900/healthz"]
interval: 30s
timeout: 5s
retries: 3Resources#
DuckDB wants memory, and a container that is being OOM-killed mid-write is the worst case for the store. Give it headroom, or cap DuckDB explicitly:
environment:
CLAWMETRY_DUCKDB_MEMORY_LIMIT: 1GB
CLAWMETRY_DUCKDB_THREADS: "2"
deploy:
resources:
limits:
memory: 2G