Skip to content

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:

  1. CLI flags--addr, --db, --retention-days, etc.
  2. Environment variablesKESTREL_ADDR, KESTREL_DB, KESTREL_RETENTION_DAYS, …
  3. kestrel.toml — see kestrel.example.toml for the full schema.

Built-in defaults pick up the gaps. Unknown TOML keys are rejected so a typo doesn't silently drop a setting.

SettingTOMLEnv varFlagDefault
HTTP listen addressaddrKESTREL_ADDR--addr:8080
SQLite pathdbKESTREL_DB--dbdata/kestrel.db
Sourcemap dirsourcemapsKESTREL_SOURCEMAPS--sourcemapsdata/sourcemaps
Data dirdataKESTREL_DATA--datadirname(db)
Retention (days)retention.daysKESTREL_RETENTION_DAYS--retention-days0 (forever)
Metrics tokenmetrics.tokenKESTREL_METRICS_TOKEN--metrics-token(empty → disabled)
Admin passwordKESTREL_ADMIN_PASSWORDauto-generated

Docker

bash
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:latest

The 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.

caddyfile
kestrel.example.com {
    reverse_proxy localhost:8080
}
nginx
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;
    }
}
yaml
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:

bash
./kestrel serve --addr :8080 --db /var/lib/kestrel/k.db

Wire 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 via KESTREL_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.

Released under the MIT License.