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

VariableDefaultNotes
STKD_BASE_URL(required)Public URL the server is reachable at. Used for OIDC redirects and webhook callbacks.
STKD_PORT80Internal listen port.
STKD_LOG_LEVELinfoerror, warn, info, debug, trace.
STKD_DATA_DIR/var/lib/stkdWhere SQLite + secrets live. Mount as a volume.

Database

VariableDefaultNotes
STKD_DATABASE_URLsqlite:///var/lib/stkd/stkd.dbUse postgres://... for Postgres.
STKD_DATABASE_POOL_MAX8Connection pool size. Postgres only.

Authentication

VariableDefaultNotes
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

VariableDefaultNotes
STKD_GITHUB_TOKEN(optional)Personal access token or GitHub App installation token.
STKD_GITLAB_TOKEN(optional)PAT with api scope.
STKD_GITLAB_HOSTgitlab.comFor 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-definition pointing to ghcr.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:latest and 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

  1. Open Google Cloud Console → APIs & Services → Credentials.
  2. Create OAuth 2.0 Client ID, type Web application.
  3. Add https://stkd.example.com/auth/callback to authorised redirect URIs.
  4. Copy client ID + secret.
  5. Set STKD_OIDC_ISSUER=https://accounts.google.com, STKD_OIDC_ALLOWED_DOMAIN=yourcorp.com.

Okta

  1. Create an OIDC app in Okta, type Web.
  2. Sign-in redirect: https://stkd.example.com/auth/callback.
  3. Copy client ID + secret.
  4. Set STKD_OIDC_ISSUER=https://yourcorp.okta.com.

Authentik / Keycloak

  1. Create a confidential OIDC client.
  2. Set the redirect URI to https://stkd.example.com/auth/callback.
  3. Copy client ID + secret.
  4. Set STKD_OIDC_ISSUER to 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.db periodically (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