SVSANNVIT
Getting Started

Hosting with Docker

Run the full Sannvit Ledger stack — Postgres, Keycloak, the ledger API, blob storage, and the dashboard — with Docker Compose.

Docker Compose is the fastest way to run a Sannvit Ledger instance, whether that's a local dev environment or a single-host production deployment. Everything below assumes you've already cloned the repo.

Prerequisites

  • Docker + Docker Compose
  • Node.js — for applying database migrations and (optionally) native dev outside containers

What's in the compose files

The stack is split into one base file plus overrides you layer on as needed:

ServiceCompose fileWhat it is
dbdocker-compose.ymlPlain postgres:17 — the only database in the stack
keycloakdocker-compose.keycloak.ymlAuth — Direct Access Grant, no hosted login UI
api / alertingdocker-compose.api.ymlThe ingest service, plus its standalone integrity/alerting runner as a second container
dashboarddocker-compose.dashboard.ymlThe browser UI
rustfsdocker-compose.rustfs.ymlBundled S3-compatible blob store — optional, see Bring your own S3-compatible storage
maildocker-compose.mail.ymlDev-only SMTP catcher (Mailpit) so invite/reset/alert emails land somewhere visible
nginx / caddydocker-compose.nginx.yml / docker-compose.caddy.ymlOptional TLS-terminating reverse proxy in front of app. / api. / auth. subdomains

The app talks to plain Postgres directly and Keycloak handles auth — no extra services in between — which keeps the footprint small enough to install on a client's own infrastructure.

1. Configure your environment

.env lives at the repo root and is read by every compose file.

cp .env.example .env

At minimum, fill in these secrets before starting anything — openssl rand -hex 24 for each:

  • POSTGRES_PASSWORD
  • JWT_SECRET
  • KEYCLOAK_ADMIN_PASSWORD
  • KEYCLOAK_SERVICE_CLIENT_SECRET
  • S3_ROOT_PASSWORD (only needed if you're using the bundled blob store)
  • EXPORT_SECRET_KEY (only needed before using the evidence-pack SFTP export feature)

Dev-only values are fine for local work — just don't reuse them anywhere real.

2. Bring up the stack

Dev

docker/run.sh manages which compose override files are active and always reads .env from the repo root, regardless of your shell's working directory:

cd docker
./run.sh config add keycloak rustfs mail api api.dev dashboard dashboard.dev
./run.sh start

api.dev / dashboard.dev layer in hot-reload (bind-mounted source, tsx watch / Nuxt HMR) on top of their base services.

Production (single host)

npm run prod

which is equivalent to:

cd docker
COMPOSE_FILE=docker-compose.yml:docker-compose.keycloak.yml:docker-compose.rustfs.yml:docker-compose.api.yml:docker-compose.dashboard.yml \
COMPOSE_ENV_FILES=../.env COMPOSE_IGNORE_ORPHANS=true \
docker compose up -d --wait --build

!WARNING Before doing this for real:

  • The values in .env.example are placeholders, not secrets — set real, unique values for every entry listed above.
  • Put a real TLS-terminating reverse proxy in front (docker-compose.nginx.yml or .caddy.yml) rather than exposing raw service ports, and set PROXY_DOMAIN in .env.
  • Change Keycloak's ledger-service client secret from the realm export's dev default (docker/volumes/keycloak/ledger-realm.json) via the Keycloak admin console, and update KEYCLOAK_SERVICE_CLIENT_SECRET in .env to match.
  • Point SMTP_* at a real relay instead of Mailpit, and set DASHBOARD_URL to the real public dashboard domain — see Email (SMTP) below.
  • Set up proper backup procedures for the db volume.

3. Apply the database schema

This is a one-time step per fresh database — not part of run.sh start. Wait for db and keycloak to report healthy (./run.sh status), then from the repo root:

for f in api/drizzle/0*.sql; do
  psql -h localhost -U postgres -d postgres -v ON_ERROR_STOP=1 -f "$f"
done

cd api
npm install
npm run db:functions   # applies src/db/functions/*.sql — not drizzle-kit objects
npm run seed:demo      # demo org + team + ingest key (key printed once, save it)

There's no seeded dashboard user — seed:demo only creates a demo org/team/ingest key for the SDK side. To get a human login, bootstrap the first admin instead (from api/, once db and keycloak are up and migrations are applied):

npm run seed:admin -- you@example.com --org-name="Your Org Name"

This prints a generated password once. Every subsequent user gets invited through the dashboard (Organization → Users → Invite) by an existing compliance_admin, not through this script.

4. You're up

  • Dashboard — http://localhost:3000
  • Ledger API — http://localhost:4010 (GET /healthz)
  • Keycloak admin — http://localhost:8180
  • Mailpit (dev email catcher) — http://localhost:8025
  • Blob store console — http://localhost:9001 (bundled RustFS, if you kept it)

Useful run.sh commands

./run.sh logs [service]     # follow logs
./run.sh status             # docker compose ps
./run.sh recreate [service] # force-recreate one service
./run.sh stop                # stop everything

Calling docker compose directly instead of run.sh needs the env file spelled out explicitly, e.g. from the repo root:

docker compose --env-file .env -f docker/docker-compose.yml -f docker/docker-compose.keycloak.yml up -d

Bring your own S3-compatible storage

By default the stack bundles a RustFS container as the blob store for payload content — the ledger itself only stores a pointer + hash. If you'd rather point at an existing bucket (AWS S3, Cloudflare R2, Backblaze B2, your own MinIO/RustFS, etc.):

  1. Don't add the rustfs override — skip ./run.sh config add rustfs, or ./run.sh config remove rustfs if it's already there.
  2. In .env, set S3_ENDPOINT, S3_PORT, S3_USE_SSL, and optionally S3_REGION to the external provider, and S3_ACCESS_KEY / S3_SECRET_KEY to credentials scoped to that bucket. Set LEDGER_BUCKET to the bucket name.
  3. The bucket is created lazily on first upload if it doesn't already exist, so either pre-create it or grant the given credentials CreateBucket permission.

The append-only second integrity plane is a second bucket (LEDGER_INTEGRITY_BUCKET, default ledger-integrity) on the same endpoint, created with S3 Object Lock enabled — undeletable for its retention window, even by admins.

!CAUTION The bundled RustFS container has open bugs in its Object Lock implementation. That's fine for dev, but production deployments should point LEDGER_INTEGRITY_BUCKET at a mature Object Lock implementation via the bring-your-own-S3 path — real AWS S3, Backblaze B2, or GCS via S3 interop.

Email (SMTP)

The ledger API sends invite, password-reset, and alert-digest emails via SMTP.

Local dev — capture emails instead of sending them:

./run.sh config add mail
./run.sh start

Every email lands at http://localhost:8025 (Mailpit) instead of a real inbox.

Production — deliver real email:

  1. Get SMTP credentials from an email provider (SendGrid, Postmark, AWS SES, Resend, Mailgun, or an existing corporate relay).
  2. Set SMTP_ADMIN_EMAIL, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, and SMTP_SENDER_NAME in that environment's .env. Leave SMTP_ALLOW_INSECURE_AUTH unset — it exists only for Mailpit.
  3. Set DASHBOARD_URL to the real public dashboard domain — it gets embedded in invite/reset email links.
  4. Don't add the mail override in production, or it'll intercept real mail.
  5. Apply the change: ./run.sh recreate api alerting.

If your provider requires domain verification (SPF/DKIM, sender identity — common with SES and Postmark), that has to be completed on the provider's side before mail will actually deliver.

Updating

  1. Review the changelog for breaking changes in the images this stack still uses (Postgres, Keycloak, RustFS, Mailpit).
  2. Update image tags in the relevant docker-compose.*.yml if needed.
  3. Pull the latest images: docker compose pull.
  4. Rebuild api / dashboard if their source changed: docker compose -f docker-compose.api.yml -f docker-compose.dashboard.yml up -d --build.

Always back up your database before updating.

Need horizontal scaling instead?

For a cluster deployment beyond one Compose host, there's a Helm-based Kubernetes path — building/pushing images, secrets, DNS, and helm install — covered separately from Docker Compose.