Self-host KRINT
Run KRINT from the pre-built image, on either registry:
- GitHub Container Registry -
ghcr.io/pianonic/krint:latest - Docker Hub -
pianonic/krint:latest
The two are identical; use whichever you prefer. Examples below use the GHCR tag.
You need a Linux/Windows host with Docker + Compose v2, and a directory to keep state in.
Pick your path
| You have… | Do this |
|---|---|
| Your own OIDC provider (Pocket ID, Authentik, Auth0, Keycloak…) | Quickstart below - three files, no clone. |
| No identity provider | Local login - leave Oidc__Authority unset, sign in with a local account. |
| A single machine, just you | The desktop app - SQLite, built-in login, no Docker auth setup. |
Quickstart
Drop these three files in an empty folder and run docker compose up -d. Open http://localhost:5000. State lives in ./db/, ./backups/, and /data/krint/ (provisioned instance data).
compose.yml
services:
krint:
image: ghcr.io/pianonic/krint:latest # or pianonic/krint:latest (Docker Hub)
container_name: krint
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway" # how krint reaches provisioned DBs
depends_on:
db:
condition: service_healthy
ports:
- "5000:8080"
environment:
Database__Provider: "Postgres"
ConnectionStrings__KrintDatabase: "Host=db;Port=5432;Database=krint;Username=postgres;Password=${POSTGRES_PASSWORD}"
Vault__MasterKey: ${KRINT_VAULT_KEY}
Krint__PublicUrl: ${KRINT_PUBLIC_URL} # login redirect + CORS are derived from this
Cors__AllowedOrigins__0: ${KRINT_PUBLIC_URL}
Oidc__Authority: ${KRINT_OIDC_AUTHORITY}
Oidc__ClientId: ${KRINT_OIDC_CLIENT_ID}
Oidc__Scope: "openid profile email roles"
Oidc__RequireHttpsMetadata: "true"
volumes:
- /var/run/docker.sock:/var/run/docker.sock # Windows: //var/run/docker.sock
- ./backups:/app/backups
- ./krint.yaml:/app/krint.yaml:ro # port ranges, storage, nodes, declarative instances
- /data/krint:/data/krint # provisioned instance data (HostFolder storage)
db:
image: postgres:18.4
container_name: krint-db
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: krint
volumes:
- ./db:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d krint"]
interval: 2s
timeout: 3s
retries: 30.env
POSTGRES_PASSWORD=change-me
KRINT_VAULT_KEY=GENERATE_ME # openssl rand -base64 32
# The public URL KRINT is served on. The login redirect URI and CORS origin are derived from it.
KRINT_PUBLIC_URL=http://localhost:5000
# Your OIDC provider:
KRINT_OIDC_AUTHORITY=https://auth.example.com/realms/krint
KRINT_OIDC_CLIENT_ID=krintkrint.yaml - the host-port ranges KRINT allocates from per engine. Copy as-is:
krint:
storage:
mode: HostFolder # provisioned data lives in /data/krint on the host (set mode: Volume for named docker volumes)
host_path: /data/krint
port_ranges:
postgres: 30000-30099
timescaledb: 30100-30199
mysql: 30200-30399
mariadb: 30400-30599
mssql: 30600-30799
mongo: 30800-30999
redis: 31000-31199
cockroachdb: 31200-31399
clickhouse: 31400-31599
cassandra: 31600-31799
couchdb: 32000-32199
neo4j: 33200-33399
qdrant: 34000-34199
valkey: 34200-34399
seaweedfs: 34400-34599
pgvector: 34600-34799
azurite: 34800-34999WARNING
With HostFolder storage, /data/krint must be writable by the engine containers, which run as fixed UIDs. If a provision fails right after the container starts, chown the folder (e.g. sudo chown -R 999:999 /data/krint for Postgres) - or switch to mode: Volume to let Docker manage it.
On your IdP, register KRINT as a public client (PKCE, no secret) with redirect URI http://localhost:5000/*. That's it - the rest is reference below.
Who gets in
KRINT has no roles of its own: anyone your IdP signs in can provision, browse and delete every database, and read every password. On a shared IdP, create a group (say krint-admin), put the operators in it, and set Oidc__AdminRole=krint-admin plus Oidc__RequireAdminRoleGlobally=true. If your IdP publishes groups under a claim other than roles, also set Oidc__RoleClaim (Pocket ID, Authentik and Entra use groups).
No OIDC provider? Local login
Leave Oidc__Authority unset and KRINT signs users in against its own user table: a login form in the UI, rotating refresh tokens, lockout after repeated failures. On first boot it creates one account:
| Variable | What it does |
|---|---|
LocalLogin__AdminUserName | Name of the first account. Default admin. |
LocalLogin__AdminPassword | Its password. Leave unset and a random one is printed once in the container log (docker logs krint). |
LocalLogin__SigningKey | Optional. Base64 key (32+ bytes) that signs the session tokens; derived from Vault__MasterKey when unset, so nothing extra to configure. |
LocalLogin__LogIssuedCredentials | true writes password reset tokens and admin-issued passwords to the log. Off by default: the log only says that one was issued. |
The quickest way to see it is the repo's own stack, which uses exactly this:
git clone https://github.com/PianoNic/KRINT.git && cd KRINT
docker compose up -d # postgres + krint on http://localhost:56722
docker logs krint # the generated password for 'admin' is printed once on first bootChange the password after signing in (Settings). Local login has no roles: every local account is an admin. Further accounts are created under Settings → Local accounts, where a password can also be reset or an account deleted; there is no self-service sign-up page. Point Oidc__Authority at an identity provider whenever you have one; the local tables stay but are no longer used for sign-in.
Configuration reference
Environment variables
Set these on the krint service (the Quickstart pulls them from .env).
| Variable | What it does |
|---|---|
Vault__MasterKey | AES-256 key for the secrets vault. 32 random bytes, base64 (openssl rand -base64 32). Encrypts every instance password. When unset, KRINT reads it from Vault__KeyFile and generates it there on first boot. |
Vault__KeyFile | Where the key lives when Vault__MasterKey is unset. Default /app/data/vault.key (the repo's compose mounts ./data there). Back this file up with your data. |
ConnectionStrings__KrintDatabase | KRINT's own metadata DB. Postgres: Host=db;Port=5432;Database=krint;Username=postgres;Password=…. SQLite: Data Source=/data/krint.db. |
Database__Provider | Postgres or Sqlite (default). Picks the metadata store. |
Oidc__Authority | Public IdP discovery URL. Unset = local login. Must match the issuer in <authority>/.well-known/openid-configuration byte-for-byte (scheme, port, trailing slash). |
Oidc__ClientId | Client ID registered on the IdP (public/PKCE). |
Oidc__RedirectUri / …PostLogoutRedirectUri | Return URL after login/logout. Must be registered on the IdP, keep the trailing slash. |
Oidc__Scope | openid profile email roles (roles optional). |
Oidc__RequireHttpsMetadata | true (set false only for a plain-HTTP IdP). |
Oidc__RoleClaim | Claim that carries group membership. Defaults to roles (Keycloak); Pocket ID, Authentik and Entra publish groups. |
Oidc__AdminRole | Name of the role/group that may use KRINT, e.g. krint-admin. Unset means every user your IdP signs in is a full admin - fine for a personal IdP, not for a shared one. |
Oidc__RequireAdminRoleGlobally | true puts Oidc__AdminRole in front of every endpoint, so users without it get a 403 instead of a working dashboard. Ignored when no admin role is set. |
Oidc__ValidateAudience | false by default: KRINT accepts whatever audience the IdP stamps on the token. Set true once the IdP puts the client ID in aud (Keycloak needs an audience mapper for that). |
Cors__AllowedOrigins__0 | Browser origin allowed to call the API - KRINT URL without trailing slash. Add more as __1, __2. |
Krint__TrustForwardedHeaders | true only behind a reverse proxy: honour X-Forwarded-For/Proto/Host from it. |
Krint__PublicUrl | Public URL this control plane is served on (e.g. https://krint.example.com). Drives the Add-node compose, and backs the OIDC redirect + CORS when those aren't set explicitly. Required to add nodes - it must be reachable from each node's host (not localhost); the Add-node dialog refuses to generate a compose until it's set. |
Backup__Directory | Where dumps are written. Optional - defaults to /app/backups (the path the compose bind-mounts). |
Backup__ExecTimeoutMinutes | Cap for a single backup or restore run inside the database container. Optional - defaults to 120. |
Docker__Endpoint | Docker daemon URI, e.g. unix:///var/run/docker.sock. Optional - auto-detected (Unix socket / Windows named pipe) when unset. |
DANGER
Never lose or rotate Vault__MasterKey once you have data. There's no recovery and no key-rotation flow.
SQLite instead of Postgres
Drop the db service and point KRINT at a file on a mounted volume:
environment:
Database__Provider: "Sqlite"
ConnectionStrings__KrintDatabase: "Data Source=/data/krint.db"
volumes:
- ./db:/dataRemove depends_on: db. The file must be on a volume or it's wiped on every recreate.
Instance data on a host folder
By default each provisioned instance gets a named Docker volume. To see the raw files instead, set in krint.yaml:
krint:
storage:
mode: HostFolder
host_path: /data/krint # path on the Docker HOSTNew provisions then bind-mount ${host_path}/${containerName}. Two gotchas: engines run as a fixed UID and need write access (chown 999:999 … for Postgres), and delete-cleanup only works if host_path is mounted into the krint container at the same path.
OIDC provider setup & quirks
KRINT's SPA uses Authorization Code Flow + PKCE, so register a public client (no secret). Configure on the IdP:
| Setting | Value |
|---|---|
| Client type | Public (PKCE) |
| Redirect / post-logout URI | http://localhost:5000/* (or your public URL) |
| Web origins / CORS | KRINT origin, no trailing slash |
| Scopes | openid profile email (roles optional) |
- Pocket ID - toggle Public Client; allow your user/group.
*wildcards work. - Authentik - use the Provider's issuer (
/application/o/<slug>/) as the authority, not the Application URL. - Auth0 - authority is
https://<tenant>.auth0.com/(trailing slash); app type SPA. - External Keycloak - authority is
https://<host>/realms/<realm>.
Using KRINT from another project or CI
Everything the UI does goes through the REST API, and the OpenAPI document is served at GET /openapi/v1.json on every deployment. Authenticate with a bearer token from the same identity provider the UI uses (for a CI job, a client-credentials token whose subject is in Oidc__AdminRole).
TOKEN=... # access token from your IdP
# find an instance by the name shown in the UI
curl -s -H "Authorization: Bearer $TOKEN" "https://krint.example.com/api/database?displayName=staging-db"
# full details incl. password and connection string
curl -s -H "Authorization: Bearer $TOKEN" "https://krint.example.com/api/database/<id>"
# provision one
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"engine":"postgres","version":"18","displayName":"staging-db"}' "https://krint.example.com/api/database/provision"Prefer configuration over scripts? Declare instances in instances.yaml and let KRINT reconcile them at startup - see Declarative instances. A provisioned instance's page also offers Copy as YAML for exactly that file.
Health and startup checks
GET /health needs no token and answers {"status":"healthy","checks":{"database":…,"docker":…}}, or 503 when the metadata database or the Docker daemon is unreachable. Point your orchestrator's probe at it.
At boot KRINT logs the effective configuration (database provider, config file, storage mode, authority, public URL) and refuses to start when Vault__MasterKey is not a 32-byte base64 key, or when it cannot decrypt the secrets already stored (master key does not match stored secrets). A missing Docker socket or Oidc__Authority is logged as an error so docker logs krint names the problem.
Reverse proxy (Caddy/Traefik/nginx)
Make the public origin match Oidc__RedirectUri and Cors__AllowedOrigins__0, serve over HTTPS, and set Krint__TrustForwardedHeaders: "true" so KRINT reads the proxy's X-Forwarded-Proto / X-Forwarded-Host instead of seeing plain http://krint:8080. Leave it unset when KRINT is reached directly - with it on, any caller could spoof its scheme and address.
krint.example.com { reverse_proxy krint:8080 }Oidc__RedirectUri=https://krint.example.com/
Oidc__PostLogoutRedirectUri=https://krint.example.com/
Oidc__RequireHttpsMetadata=true
Cors__AllowedOrigins__0=https://krint.example.comWhen the identity provider runs on the same host, keep Oidc__Authority as the public HTTPS issuer URL the browser sees and set Oidc__InternalAuthority to the in-cluster URL KRINT uses for discovery.
Operations
Upgrade
docker compose pull krint && docker compose up -d krintMigrations run on startup; the vault, metadata, and provisioned containers are preserved. Pin a version by replacing :latest with a published tag.
Back up the ./db/ (or postgres-data volume) and ./backups/ directories before major upgrades. Provisioned-instance volumes are independent and survive docker compose down.
Schedule dumps from the Backups page - they land in ./backups/.
Provisioned containers are labelled as a Compose project named krint-databases, so Docker Desktop (and docker compose ls) groups them into their own cluster - separate from KRINT's own stack - instead of listing them as loose containers.
Troubleshooting
Common errors & fixes
| Symptom | Fix |
|---|---|
Vault:MasterKey must decode to 32 bytes | Regenerate with openssl rand -base64 32. |
Vault:MasterKey does not match stored secrets | KRINT started with a different key than the one the secrets were written with. Restore the original Vault__MasterKey or vault.key. A stack started from the repo's compose.yml before it generated keys used the old demo key 2P+vvGUw5v3weZdFU04YOicpVNs9eIyRKo6NliiDR4c=: set that in .env to read its data. |
401 invalid_token: issuer is invalid | Oidc__Authority must match the IdP's issuer byte-for-byte. |
401: signature key was not found | API can't reach JWKS - check Oidc__InternalAuthority / KC_HOSTNAME_BACKCHANNEL_DYNAMIC. |
| "You're not allowed to access this service" | IdP authenticated the user but the client policy denies them - allow the user/group. |
CORS error on /api/* | Cors__AllowedOrigins__0 must match the SPA origin (no trailing slash). |
Cannot connect to the Docker daemon | The /var/run/docker.sock bind is missing from the krint service. |
<engine> container did not become ready on provision | KRINT reaches provisioned DBs over its own Docker network (the default compose network) and falls back to the host port - keep extra_hosts: ["host.docker.internal:host-gateway"] on the krint service for that fallback. |
SocketException (13): Permission denied | Running as non-root without socket access - keep the default root user or group_add the docker GID. |
No free host port in range | krint.yaml port_ranges exhausted - delete an instance or widen the range. |
| DB auth fails after changing the password | POSTGRES_PASSWORD only applies on first init - reset in-DB or wipe the volume. |
Your app gets Name or service not known or Connection refused for a provisioned DB | The host shown for an instance is only an address on the machine running the container. Connect by container name on KRINT's Docker network. See Connect an app to a node-hosted database. |
permission denied for schema public on your app's migrations | Postgres 15+ dropped the implicit CREATE grant. Make your app's role the owner of its database. |
See also: Declarative instances · Nodes · Developer setup