The stkd CLI is great for one developer. When several developers on a team start stacking work in the same repos, the team wants a view: which stacks are open, who’s waiting on review, which PRs have merged, where the conflicts are. stkd ships an optional web dashboard that gives you exactly that, running on your own infrastructure.
This guide walks through deploying it.
What you get
The dashboard is a small Rust service (stkd-server) with:
- A team overview of every open stack across every repo you’ve connected (GitHub or GitLab).
- A per-stack drilldown showing the branches, PRs, statuses, conflicts, and reviewer assignments.
- A live PR status board updated via WebSocket as provider events come in.
- A search across stacks by author, reviewer, label, or branch name.
- OIDC authentication so your existing SSO covers it.
- A REST API + WebSocket stream so you can build dashboards of your own.
It’s deliberately not a code-review UI — your team already has GitHub or GitLab for that. The dashboard is a coordination layer above provider tooling, not a replacement for it.
Quickest path: docker run
docker run -d \
--name stkd-server \
-p 8080:80 \
-v stkd-data:/var/lib/stkd \
-e STKD_BASE_URL=https://stkd.example.com \
-e STKD_OIDC_ISSUER=https://accounts.google.com \
-e STKD_OIDC_CLIENT_ID=...apps.googleusercontent.com \
-e STKD_OIDC_CLIENT_SECRET=... \
-e STKD_GITHUB_TOKEN=ghp_... \
ghcr.io/neul-labs/stkd-server:latest
That’s it. Visit https://stkd.example.com, sign in with your OIDC provider, and authorise the dashboard against the GitHub or GitLab repos you want to track.
For most teams that’s all they ever do.
Configuration reference
The server reads configuration from environment variables (12-factor style). Below are the ones you’ll actually touch.
Core
| Variable | Default | Notes |
|---|---|---|
STKD_BASE_URL | (required) | Public URL the server is reachable at. Used for OIDC redirects and webhook callbacks. |
STKD_PORT | 80 | Internal listen port. |
STKD_LOG_LEVEL | info | error, warn, info, debug, trace. |
STKD_DATA_DIR | /var/lib/stkd | Where SQLite + secrets live. Mount as a volume. |
Database
| Variable | Default | Notes |
|---|---|---|
STKD_DATABASE_URL | sqlite:///var/lib/stkd/stkd.db | Use postgres://... for Postgres. |
STKD_DATABASE_POOL_MAX | 8 | Connection pool size. Postgres only. |
Authentication
| Variable | Default | Notes |
|---|---|---|
STKD_OIDC_ISSUER | (required) | OIDC issuer URL, e.g. https://accounts.google.com. |
STKD_OIDC_CLIENT_ID | (required) | |
STKD_OIDC_CLIENT_SECRET | (required) | |
STKD_OIDC_ALLOWED_DOMAIN | (optional) | If set, only emails from this domain can sign in. |
STKD_ADMIN_TOKEN | (optional) | Static bearer for CI / scripted integrations. |
Provider integration
| Variable | Default | Notes |
|---|---|---|
STKD_GITHUB_TOKEN | (optional) | Personal access token or GitHub App installation token. |
STKD_GITLAB_TOKEN | (optional) | PAT with api scope. |
STKD_GITLAB_HOST | gitlab.com | For self-hosted GitLab. |
Postgres deployment
SQLite is fine up to ~20 active engineers. Beyond that, switch to Postgres:
# docker-compose.yml
services:
stkd:
image: ghcr.io/neul-labs/stkd-server:latest
environment:
STKD_BASE_URL: https://stkd.example.com
STKD_DATABASE_URL: postgres://stkd:secret@db:5432/stkd
STKD_OIDC_ISSUER: https://auth.example.com
STKD_OIDC_CLIENT_ID: ${OIDC_ID}
STKD_OIDC_CLIENT_SECRET: ${OIDC_SECRET}
depends_on: [db]
ports: ["8080:80"]
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: stkd
POSTGRES_PASSWORD: secret
POSTGRES_DB: stkd
volumes: ["pg-data:/var/lib/postgresql/data"]
volumes:
pg-data: {}
Migrations are bundled in the binary and applied automatically on startup. There is no separate migration step.
Kubernetes
A bare-minimum Helm-style manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: stkd-server
spec:
replicas: 2
selector:
matchLabels: { app: stkd-server }
template:
metadata:
labels: { app: stkd-server }
spec:
containers:
- name: stkd
image: ghcr.io/neul-labs/stkd-server:latest
env:
- name: STKD_BASE_URL
value: https://stkd.example.com
- name: STKD_DATABASE_URL
valueFrom: { secretKeyRef: { name: stkd-secrets, key: db } }
- name: STKD_OIDC_ISSUER
value: https://auth.example.com
- name: STKD_OIDC_CLIENT_ID
valueFrom: { secretKeyRef: { name: stkd-secrets, key: oidc-id } }
- name: STKD_OIDC_CLIENT_SECRET
valueFrom: { secretKeyRef: { name: stkd-secrets, key: oidc-secret } }
ports: [{ containerPort: 80 }]
readinessProbe:
httpGet: { path: /healthz, port: 80 }
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 512Mi }
If you run multiple replicas, you’ll need Postgres (not SQLite). The WebSocket layer uses Postgres LISTEN/NOTIFY to fan events across replicas.
CapRover, Coolify, Dokku, fly.io, Render
The container is a stock OCI image listening on port 80, so any PaaS that takes a container deploys it cleanly:
- CapRover: drop a one-line
captain-definitionpointing toghcr.io/neul-labs/stkd-server:latest. Configure env vars in the dashboard, attach a persistent volume. - Coolify / Dokku: add the container as an app, set env vars, attach a volume.
- fly.io:
fly launch --image ghcr.io/neul-labs/stkd-server:latestand add a Postgres add-on. - Render: web service from a Docker image, plus a Render Postgres.
You can also build from source if you’d rather:
git clone https://github.com/neul-labs/stkd
cd stkd
cargo build --release -p stkd-server
./target/release/stkd-server
OIDC providers — concrete recipes
The trickiest part of any self-hosted dashboard is wiring SSO. Some recipes:
Google Workspace
- Open Google Cloud Console → APIs & Services → Credentials.
- Create OAuth 2.0 Client ID, type Web application.
- Add
https://stkd.example.com/auth/callbackto authorised redirect URIs. - Copy client ID + secret.
- Set
STKD_OIDC_ISSUER=https://accounts.google.com,STKD_OIDC_ALLOWED_DOMAIN=yourcorp.com.
Okta
- Create an OIDC app in Okta, type Web.
- Sign-in redirect:
https://stkd.example.com/auth/callback. - Copy client ID + secret.
- Set
STKD_OIDC_ISSUER=https://yourcorp.okta.com.
Authentik / Keycloak
- Create a confidential OIDC client.
- Set the redirect URI to
https://stkd.example.com/auth/callback. - Copy client ID + secret.
- Set
STKD_OIDC_ISSUERto your realm’s issuer URL.
Quick-and-dirty admin mode (development only)
For local testing without OIDC:
STKD_ADMIN_TOKEN=devtoken stkd-server
Then visit https://stkd.local/?admin=devtoken. Don’t use this in production.
Provider webhooks
For instant updates (instead of poll-based status refresh), wire your repos’ webhooks to:
POST https://stkd.example.com/webhook/github
POST https://stkd.example.com/webhook/gitlab
Use the secret printed by stkd-server --print-webhook-secret. Once webhooks are flowing, the dashboard updates in real-time as PRs change status.
Backups
The server is stateful: it has tokens, OIDC sessions, and a cache of provider state. Back it up:
- SQLite: dump
STKD_DATA_DIR/stkd.dbperiodically (sqlite3 stkd.db .dump). - Postgres: standard
pg_dump. - Secrets: tokens encrypted with a key in
STKD_DATA_DIR/secrets.key— back that up separately from the DB and store it like any other production secret.
In a disaster, restore both files, restart, and the dashboard is up.
Observability
The server exposes:
/healthz— liveness probe./readyz— DB-reachable readiness probe./metrics— Prometheus exposition with the usual HTTP + DB metrics, plus stkd-specific counters (stkd_stacks_open,stkd_pr_status_total, etc.).- JSON-structured logs to stdout, ready to ship to Loki / CloudWatch / whatever.
What’s not in the dashboard
- In-line code review — that’s your provider’s UI.
- CI execution — that’s your CI’s job.
- Branch deletion / repo administration — never.
- A merge queue — for that, use GitHub’s merge queue or GitLab’s merge train.
The dashboard is intentionally a thin coordination layer. The boring choice; the right one.
Roadmap
In the works: per-repo Slack notifications (configurable per-team), a “stack health” page (stale stacks, conflicting stacks), and an audit log of every mutation made via the API.
Track the GitHub project for status.
Where to next
- Install stkd CLI — the dashboard is optional but the CLI is the workflow.
- Migrating from Graphite — if you’re moving from Graphite’s hosted dashboard to your own.
- stkd MCP server — let agents drive your stacks.