Self-hosting
Keystrok is built to run on your own machine, next to the keys it watches. One Docker stack: the app, Postgres, and a local mail catcher. No external services required to get started.
Quick start
git clone https://github.com/Rootzreggae/keystrok && cd keystrok
cp .env.example .env
echo "NEXTAUTH_SECRET=$(openssl rand -base64 32)" >> .env
echo "ENCRYPTION_KEY=$(openssl rand -base64 32)" >> .env
echo "ALLOWED_EMAILS=you@example.com" >> .env
docker compose up --build
Then:
- App:
http://localhost:3001 - Magic-link inbox (Mailpit):
http://localhost:8025
Sign in with the email you allowlisted. The sign-in link lands in the Mailpit inbox at :8025, not your real inbox. The first user to sign in becomes the admin.
Prefer the prebuilt image? docker compose pull && docker compose up uses the published ghcr.io/rootzreggae/keystrok:latest instead of building locally.
Mail: the one thing that will surprise you
The bundled mail catcher (Mailpit) traps all outgoing email locally. That is what makes solo use work with zero email setup, and it is also why team invites will never reach a real inbox until you configure real delivery. When you are ready to invite people, set either:
EMAIL_SERVER_HOST/EMAIL_SERVER_PORT/EMAIL_SERVER_USER/EMAIL_SERVER_PASSWORD(any SMTP server), orRESEND_API_KEY(Resend)
The Team page always shows where mail is currently being delivered, so you can check what your instance is actually doing.
Environment variables
| Variable | Purpose |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
NEXTAUTH_URL |
The URL your instance is served at |
NEXTAUTH_SECRET |
Session signing secret (openssl rand -base64 32) |
ENCRYPTION_KEY |
AES-256-GCM key for credentials stored at rest (openssl rand -base64 32) |
ALLOWED_EMAILS |
Comma-separated sign-in allowlist |
ALLOWED_EMAIL_DOMAINS |
Allowlist whole domains instead of single addresses |
AUTH_OPEN_REGISTRATION |
true opens sign-up to anyone. One instance is one shared workspace, so only do this if you mean it |
EMAIL_SERVER_* / EMAIL_FROM |
SMTP for magic links and invites |
RESEND_API_KEY |
Alternative to SMTP |
CRON_SECRET |
Authorizes the scheduled-job endpoints below |
ALLOW_PRIVATE_PLATFORM_URLS |
Lets platform base URLs point at private networks. Off by default as an SSRF guard; turn it on only if your Grafana or similar genuinely lives on a private address |
Scheduled jobs
Keystrok does not ship a scheduler. If you want continuous behavior, point anything that can run a curl on a timer (cron, systemd timers, your NAS) at these endpoints with your CRON_SECRET as a Bearer token or ?key=:
POST /api/cron/scanre-scans every connected GitHub source and surfaces new leaks into Discovery. It is heavy (clones repos), so daily is a good cadence. Each source has a 6-hour floor, so an over-eager schedule cannot thrash.POST /api/cron/tickis the light pass: liveness re-checks and alert delivery. Run it as often as you like; every 15 minutes is plenty.
Without a scheduler, everything still works manually: scans run when you click them.
Updating
Pull the new image and restart. Schema changes apply themselves at boot (prisma db push runs in the entrypoint before the server starts).
docker compose pull && docker compose up -d
Your data lives in the Postgres volume. Keystrok never stores full secret values, but the database does hold your platform credentials (encrypted) and your findings history, so back the volume up like you would any database.