Self-hosting
Kestrel is a single binary; the entire deployment surface is one process, one SQLite file, and one directory of uploaded sourcemaps. There is nothing to cluster, no dependency to install, no message broker to provision.
Configuration
All configuration is read from three sources, highest precedence first:
- CLI flags —
--addr,--db,--retention-days, etc. - Environment variables —
KESTREL_ADDR,KESTREL_DB,KESTREL_RETENTION_DAYS, … kestrel.toml— seekestrel.example.tomlfor the full schema.
Built-in defaults pick up the gaps. Unknown TOML keys are rejected so a typo doesn't silently drop a setting.
| Setting | TOML | Env var | Flag | Default |
|---|---|---|---|---|
| HTTP listen address | addr | KESTREL_ADDR | --addr | :8080 |
| SQLite path | db | KESTREL_DB | --db | data/kestrel.db |
| Sourcemap dir | sourcemaps | KESTREL_SOURCEMAPS | --sourcemaps | data/sourcemaps |
| Data dir | data | KESTREL_DATA | --data | dirname(db) |
| Retention (days) | retention.days | KESTREL_RETENTION_DAYS | --retention-days | 0 (forever) |
| Metrics token | metrics.token | KESTREL_METRICS_TOKEN | --metrics-token | (empty → disabled) |
| Admin password | — | KESTREL_ADMIN_PASSWORD | — | auto-generated |
Docker
docker run -d \
--name kestrel \
--restart unless-stopped \
-p 8080:8080 \
-v kestrel-data:/data \
-e KESTREL_ADMIN_PASSWORD=please-change-me \
-e KESTREL_RETENTION_DAYS=90 \
ghcr.io/wearzdk/kestrel:latestThe image exposes 8080 and writes everything under /data (DB + sourcemaps + admin password file + session secret). Mount a named volume there and the container is fully stateless.
Docker Hub mirror: wearzdk/kestrel:latest (when configured by the maintainer).
Reverse proxy + TLS
Kestrel does not terminate TLS itself — put it behind a reverse proxy you already trust. The ingest path needs to be reachable from your apps' network; the admin UI typically only needs to be reachable from yours.
kestrel.example.com {
reverse_proxy localhost:8080
}server {
listen 443 ssl http2;
server_name kestrel.example.com;
ssl_certificate /etc/letsencrypt/live/kestrel.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/kestrel.example.com/privkey.pem;
# SDK clients send <= 256 KB envelopes; bump the limit a touch for headroom.
client_max_body_size 1m;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}services:
kestrel:
image: ghcr.io/wearzdk/kestrel:latest
volumes:
- kestrel-data:/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.kestrel.rule=Host(`kestrel.example.com`)"
- "traefik.http.routers.kestrel.entrypoints=websecure"
- "traefik.http.routers.kestrel.tls.certresolver=letsencrypt"
- "traefik.http.services.kestrel.loadbalancer.server.port=8080"Static binary
If Docker isn't your preference, every release ships static binaries on the GitHub Releases page. Pick the archive matching your OS and arch, untar it, and run:
./kestrel serve --addr :8080 --db /var/lib/kestrel/k.dbWire it up under systemd if you want auto-start; the binary handles SIGINT/SIGTERM cleanly.
Auth model
Kestrel ships with two authentication classes:
- Admin password — guards the web UI and all
/api/v1/{projects,issues,events}reads/writes. Set viaKESTREL_ADMIN_PASSWORD; if unset, a 24-character password is generated on first run, written to/<data>/.admin_password, and printed to the log. - Project tokens — guard SDK ingest, sourcemap uploads, and MCP. Stored as sha256 hashes; the plaintext is shown exactly once at create / rotate time. Rotating revokes the old token immediately.
There is no third class. Anonymous reads are not a feature — every read path requires an admin cookie.