Deploying
groundtruth is a single binary (gt). There is no separate agent or runtime to install alongside it. You can run it two ways: as a scheduled one-shot (gt run) that reports to an external monitor, or as a long-running daemon (gt watch) that exposes HTTP endpoints for your own tooling (see Integrating).
Deploy free with GitHub Actions
The lightest way to run groundtruth is on a schedule with no server at all. gt init scaffolds a starter config and a GitHub Actions workflow that runs your checks every 15 minutes:
gt initThis writes groundtruth.hcl and .github/workflows/groundtruth.yml. The config includes a heartbeat block; the workflow installs gt and runs gt run groundtruth.hcl on a cron schedule.
Two steps to go live:
- Create a free check at a cron-monitor (Better Stack, healthchecks.io, …) and copy its ping URL.
- In your repo, add two Actions secrets:
DATABASE_URL(your database) andHEARTBEAT_URL(the ping URL). Push.
Every run pings the monitor: green when checks pass, a failure report when they don’t. Because the monitor also expects the ping on schedule, a workflow that fails to start — a bad credential, a broken runner — pages you through the same channel. Scheduled Actions are free on public repositories and included in the free tier for private ones.
This lane has no always-on process, so it can’t serve the /metrics and /checks endpoints. When you want those — for Prometheus scraping or a Kubernetes readiness probe — move to gt watch below.
systemd
Run gt watch as a service:
[Unit]Description=groundtruthAfter=network-online.target
[Service]ExecStart=/usr/local/bin/gt watch /etc/groundtruth/config.hcl --addr 0.0.0.0:9090Environment=DATABASE_URL=postgres://user:pass@db:5432/appEnvironment=GROUNDTRUTH_TOKEN=a-long-random-secretRestart=on-failure
[Install]WantedBy=multi-user.targetgroundtruth shuts down gracefully on SIGINT/SIGTERM, so systemctl stop and restarts are clean.
Docker
Mount your config and pass credentials as environment variables:
docker run --rm \ -p 9090:9090 \ -e DATABASE_URL="postgres://user:pass@db:5432/app" \ -e GROUNDTRUTH_TOKEN="a-long-random-secret" \ -v "$PWD/config.hcl:/etc/groundtruth/config.hcl:ro" \ ghcr.io/jondot/groundtruth:latest \ watch /etc/groundtruth/config.hcl --addr 0.0.0.0:9090Kubernetes
A complete, ready-to-apply setup: config in a ConfigMap, credentials in a Secret, the daemon in a Deployment, and a Service in front.
apiVersion: v1kind: ConfigMapmetadata: name: groundtruth-configdata: config.hcl: | connection "postgres" "main" { dsn = env("DATABASE_URL") }
check "orders_present" { on = connection.postgres.main query = "select count(*) as n from orders" fail = row.n == 0 }---apiVersion: v1kind: Secretmetadata: name: groundtruth-secretstype: OpaquestringData: DATABASE_URL: "postgres://user:pass@db:5432/app" GROUNDTRUTH_TOKEN: "a-long-random-secret"---apiVersion: apps/v1kind: Deploymentmetadata: name: groundtruthspec: replicas: 1 selector: matchLabels: app: groundtruth template: metadata: labels: app: groundtruth spec: containers: - name: groundtruth image: ghcr.io/jondot/groundtruth:latest args: - watch - /etc/groundtruth/config.hcl - --addr - 0.0.0.0:9090 ports: - containerPort: 9090 envFrom: - secretRef: name: groundtruth-secrets livenessProbe: httpGet: path: /healthz port: 9090 readinessProbe: httpGet: path: /checks port: 9090 volumeMounts: - name: config mountPath: /etc/groundtruth readOnly: true volumes: - name: config configMap: name: groundtruth-config---apiVersion: v1kind: Servicemetadata: name: groundtruthspec: selector: app: groundtruth ports: - port: 9090 targetPort: 9090Liveness uses /healthz (always open, always 200 while the process lives). Readiness uses /checks, which returns 503 when any check is FAIL or ERROR, so the pod leaves the Service until data is healthy again.
Resilience
groundtruth is built to keep running when the databases it watches misbehave:
- A database goes down. Its checks become ERROR and
/checksreturns503. The daemon does not crash — it keeps serving and keeps running the checks that target other connections. - A query hangs. Each check is bounded by its
timeout(default 30s). A hung query is reported as ERROR after the timeout, not held forever. - One check breaks. Checks run concurrently and in isolation, so a single failing or panicking check can’t crash the daemon or take down the others.
Persisting state in production
By default groundtruth keeps its failing-check state in memory, which is lost on restart. For production, add a persisted state store so sustained timers and failing history survive restarts. See Securing & persisting.