Self-host
shush is a four-service stack you can run on any container platform:
- postgres — primary store (encrypted secret values, audit log).
- redis — session store + device-code flow.
- api — Bun + Hono backend.
- web — Vite + nginx frontend.
This page covers two common setups: Docker Compose (for evaluation / small deployments) and Dokploy (for managed, domain-routed prod).
1. Generate secrets
Run these locally and store the output somewhere safe (1Password, AWS Secrets Manager, Vault — not in the same shush instance you’re about to boot):
openssl rand -base64 32 # → MASTER_KEK_B64 (wraps every org's DEK)
openssl rand -base64 32 # → BETTER_AUTH_SECRET (signs sessions)Losing
MASTER_KEK_B64permanently destroys every stored secret. Back it up to a separate, audited secret store — not in this app.
2. Docker Compose
The repo ships a production docker-compose.yml that builds and runs all
four services:
docker compose up --buildThis wires up:
postgres:16-alpineon:5432(volumeshush_postgres_data).redis:7-alpineon:6379with AOF persistence (volumeshush_redis_data).- The API on
:8787, built fromapps/api/Dockerfile. - The web on
:5173 → 80, built fromapps/web/Dockerfile.
Required env vars for docker compose up:
BETTER_AUTH_SECRET=…
MASTER_KEK_B64=…
RESEND_API_KEY=… # for invitation / 2FA emails
EMAIL_FROM=shush <no-reply@yourdomain.com>Optional, with sensible defaults:
APP_URL=http://localhost:5173
BETTER_AUTH_URL=http://localhost:8787
CORS_ORIGINS=http://localhost:5173
VITE_API_URL=http://localhost:8787The API depends on postgres + redis being healthy before it starts — the compose file handles that with healthchecks.
3. Dokploy
The repo also includes DOKPLOY.md with a step-by-step guide for hosting
on Dokploy. The shape:
a. Postgres (managed DB)
- Postgres 16, database
shush, usershush, strong password. - Internal URL:
postgres://shush:<password>@shush-postgres:5432/shush.
b. Redis (managed DB)
- Redis 7 with AOF persistence on.
- Internal URL:
redis://shush-redis:6379.
c. API service
-
Type: Application → Dockerfile.
-
Build path:
/, Dockerfile:apps/api/Dockerfile. -
Internal port:
8787. -
Domain:
api.shush.yourdomain.com(HTTPS via Traefik / Lets Encrypt). -
Env:
NODE_ENV=production PORT=8787 DATABASE_URL=postgres://shush:<password>@shush-postgres:5432/shush REDIS_URL=redis://shush-redis:6379 APP_URL=https://shush.yourdomain.com BETTER_AUTH_URL=https://api.shush.yourdomain.com BETTER_AUTH_SECRET=… MASTER_KEK_B64=… CORS_ORIGINS=https://shush.yourdomain.com RESEND_API_KEY=… EMAIL_FROM=shush <no-reply@yourdomain.com> -
Pre-deploy hook (one-time, then disable):
cd apps/api && bun run db:pushOr commit migrations with
bun run db:generateand shipdb:migrate.
d. Web service
- Type: Application → Dockerfile,
apps/web/Dockerfile. - Build arg:
VITE_API_URL=https://api.shush.yourdomain.com. - Internal port:
80. - Domain:
shush.yourdomain.com.
4. Deploy order
- Postgres + Redis first (must be healthy before the API boots).
- Run the API’s
db:pushpre-deploy. - Start the API.
- Build the web with
VITE_API_URLpointing at the API’s public domain.
5. Verify
curl https://api.shush.yourdomain.com/health
# → {"ok":true,"checks":{"postgres":"ok","redis":"ok"}}6. Rotating MASTER_KEK_B64
MASTER_KEK_B64 wraps each org’s DEK — not the secret values directly.
Rotation is therefore cheap (one row per org):
- Generate a new
MASTER_KEK_B64. - Run
apps/api/scripts/rotate-kek.tswithOLD_MASTER_KEK_B64andMASTER_KEK_B64set. - Roll the env var, redeploy.
7. Backups
- Use your platform’s managed-DB backup schedule (daily, retain 30d).
- Encrypted columns (
secret_value_version.ciphertext) are unreadable withoutMASTER_KEK_B64— back up the KEK separately, in a system with its own access controls and audit log.