Single-tenant ClawMetry inside your VPC or on-prem box. One container serves the dashboard UI and the ingest API your node daemons push to. Data never leaves the deployment (see "What leaves the box" below).
- Docker + Docker Compose v2 (
docker compose) - 1 vCPU / 1 GB RAM is plenty for tens of nodes; disk sized to your event retention (events are small JSON rows; budget ~1 GB per 5M events)
- A TLS-terminating reverse proxy (nginx/Caddy/ALB) in front of port 8900 if nodes connect across the network, the container itself speaks plain HTTP
cd deploy/self-hosted
cat > .env <<'EOF'
CLAWMETRY_API_TOKENS=cm_generate_a_long_random_token_here
CLAWMETRY_ADMIN_USER=admin
CLAWMETRY_ADMIN_PASSWORD=generate_a_long_random_password
EOF
docker compose up -dVerify:
curl -u admin:...password... http://localhost:8900/api/selfhosted/statusOn each machine running agents:
pip install clawmetry
CLAWMETRY_ENDPOINT=https://clawmetry.internal.example \
clawmetry connect --key cm_generate_a_long_random_token_hereCLAWMETRY_ENDPOINT can also be persisted as the "endpoint" key in
~/.clawmetry/config.json on the node. Resolution order everywhere:
CLAWMETRY_ENDPOINT env > CLAWMETRY_INGEST_URL env (legacy) > config
endpoint > managed cloud default.
Tokens must start with cm_ (the CLI enforces the prefix). Multiple tokens
are comma-separated in CLAWMETRY_API_TOKENS; rotate by adding the new token,
re-connecting nodes, then removing the old one and restarting the server.
Each node keeps its own full local dashboard at localhost:8900 as usual.
The self-hosted server records every node's heartbeats, sessions, and events
(SQLite) and serves a fleet overview page at /selfhosted (admin login:
node roster, daemon versions, liveness) plus fleet APIs:
/api/selfhosted/nodes, /api/selfhosted/status, /api/export/events.
If a fleet-wide daemon install is not going to clear your security review, this deployment also accepts data straight from the runtime. Developers install nothing; you push one config value:
CLAWMETRY_TELEMETRY=1
OTEL_EXPORTER_OTLP_ENDPOINT=https://clawmetry.internal.example
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <your gateway token>"
OTEL_RESOURCE_ATTRIBUTES="team.id=platform,repository=payments-api"The Authorization header is required from anywhere but the loopback
interface (/v1/* is gated like /api/*), and a refused exporter drops its
batch silently, so get it right before rolling this out to a fleet.
Records land on /v1/logs and persist to DuckDB with the identity the runtime
sends (user, email, organization, session) plus your own team/repo attributes,
so GET /api/otel/rollup?dimension=team answers "what did this team spend"
from the ingested rows alone. The image already contains the protobuf decoder
this needs.
Two things to know before you plan around it. It covers Claude Code and
Codex, the runtimes that emit OpenTelemetry natively; the rest need the
daemon that reads their transcripts. And records arrive in plaintext: the
runtime encrypts nothing before it sends, so the CLAWMETRY_SELF_HOSTED_E2E
option below does not apply to this path. Inside a VPC deployment the
plaintext never leaves your network, which is exactly why this shape and
self-hosting go together.
By default the server answers /auth with "e2e": false, so nodes send
events in plaintext inside your deployment, that is what makes
server-side audit export and fleet queries meaningful. Set
CLAWMETRY_SELF_HOSTED_E2E=1 to keep client-side AES-256-GCM blob
encryption instead; the server then stores opaque ciphertext and audit
export contains only envelope metadata (node ids, timestamps, paths).
Outbound calls from a self-hosted deployment: none, with one optional
exception. If you set CLAWMETRY_LICENSE_PING=1, the server POSTs once per
24h to https://app.clawmetry.com/api/license/ping exactly this payload:
{"kind": "selfhosted_ping", "version": "<package version>",
"license": "<license 'sub' claim or empty>", "tier": "<license tier or empty>",
"ts": "<ISO-8601>"}No hostnames, node ids, counts, event data, or metrics, ever. The ping is off by default. Node daemons pointed at your endpoint make no calls to clawmetry.com either (install telemetry and anonymous analytics short-circuit when a custom endpoint is configured).
# JSONL (one event per line)
curl -u admin:... "http://localhost:8900/api/export/events?from=2026-07-01&to=2026-07-31" \
> events.jsonl
# Or from any workstation with the CLI:
CLAWMETRY_ENDPOINT=https://clawmetry.internal.example \
CLAWMETRY_API_KEY=cm_... \
clawmetry export --from 2026-07-01 --to 2026-07-31 --format csv --out events.csvThe export reads the append-only event log (ingest_log + events tables);
rows are never updated or deleted by the server.
Each release publishes a signed image, ghcr.io/vivekchand/clawmetry, and once
a digest has passed anonymous-pull verification the release opens a pull
request pinning this Compose file to it
(image: ghcr.io/vivekchand/clawmetry:<version>@sha256:<digest>). Check the
image before you run it and upgrade by digest:
docs/self-hosting.md.
With a pinned digest:
cd deploy/self-hosted
docker compose pull
docker compose up -d # recreates the container; the volume persistsRoll back by setting image: back to the previous digest. While this file
still has a build: block, no image has passed verification yet and Compose
builds from your checkout:
git pull # or check out the release tag you validated
docker compose build --pull
docker compose up -dTo build from source after the file is pinned, add the override:
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build.
Schema migrations are additive (CREATE TABLE IF NOT EXISTS); downgrades are
not supported, snapshot the volume before upgrading (see Backups).
All state lives on the clawmetry-data volume (/root/.clawmetry in the
container): selfhosted.db (SQLite ingest/audit store), events.duckdb
(local event store), config.json. The image has no sqlite3 binary;
Python's backup API is safe with SQLite in WAL mode:
docker compose exec clawmetry python3 -c "import sqlite3; s = sqlite3.connect('/root/.clawmetry/selfhosted.db'); d = sqlite3.connect('/root/.clawmetry/backup.db'); s.backup(d); d.close(); s.close()"
docker cp "$(docker compose ps -q clawmetry)":/root/.clawmetry/backup.db ./or stop the container and snapshot the whole volume. Test restores by mounting the volume into a scratch compose project.
- The fleet-level UI is intentionally minimal (JSON APIs); each node's own dashboard remains the rich per-node UI. Full multi-node dashboard parity with ClawMetry Cloud is on the Enterprise roadmap.
- No Kubernetes/Helm yet, this compose file is the supported deployment.
- What is and is not available (SSO, platform quickstarts, offline install): docs/self-hosting.md.