Skip to content

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 providerLocal login - leave Oidc__Authority unset, sign in with a local account.
A single machine, just youThe 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

yaml
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

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=krint

krint.yaml - the host-port ranges KRINT allocates from per engine. Copy as-is:

yaml
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-34999

WARNING

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:

VariableWhat it does
LocalLogin__AdminUserNameName of the first account. Default admin.
LocalLogin__AdminPasswordIts password. Leave unset and a random one is printed once in the container log (docker logs krint).
LocalLogin__SigningKeyOptional. Base64 key (32+ bytes) that signs the session tokens; derived from Vault__MasterKey when unset, so nothing extra to configure.
LocalLogin__LogIssuedCredentialstrue 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:

bash
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 boot

Change 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).

VariableWhat it does
Vault__MasterKeyAES-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__KeyFileWhere 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__KrintDatabaseKRINT's own metadata DB. Postgres: Host=db;Port=5432;Database=krint;Username=postgres;Password=…. SQLite: Data Source=/data/krint.db.
Database__ProviderPostgres or Sqlite (default). Picks the metadata store.
Oidc__AuthorityPublic IdP discovery URL. Unset = local login. Must match the issuer in <authority>/.well-known/openid-configuration byte-for-byte (scheme, port, trailing slash).
Oidc__ClientIdClient ID registered on the IdP (public/PKCE).
Oidc__RedirectUri / …PostLogoutRedirectUriReturn URL after login/logout. Must be registered on the IdP, keep the trailing slash.
Oidc__Scopeopenid profile email roles (roles optional).
Oidc__RequireHttpsMetadatatrue (set false only for a plain-HTTP IdP).
Oidc__RoleClaimClaim that carries group membership. Defaults to roles (Keycloak); Pocket ID, Authentik and Entra publish groups.
Oidc__AdminRoleName 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__RequireAdminRoleGloballytrue 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__ValidateAudiencefalse 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__0Browser origin allowed to call the API - KRINT URL without trailing slash. Add more as __1, __2.
Krint__TrustForwardedHeaderstrue only behind a reverse proxy: honour X-Forwarded-For/Proto/Host from it.
Krint__PublicUrlPublic 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__DirectoryWhere dumps are written. Optional - defaults to /app/backups (the path the compose bind-mounts).
Backup__ExecTimeoutMinutesCap for a single backup or restore run inside the database container. Optional - defaults to 120.
Docker__EndpointDocker 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:

yaml
    environment:
      Database__Provider: "Sqlite"
      ConnectionStrings__KrintDatabase: "Data Source=/data/krint.db"
    volumes:
      - ./db:/data

Remove 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:

yaml
krint:
  storage:
    mode: HostFolder
    host_path: /data/krint     # path on the Docker HOST

New 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:

SettingValue
Client typePublic (PKCE)
Redirect / post-logout URIhttp://localhost:5000/* (or your public URL)
Web origins / CORSKRINT origin, no trailing slash
Scopesopenid 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).

bash
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.

caddy
krint.example.com { reverse_proxy krint:8080 }
env
Oidc__RedirectUri=https://krint.example.com/
Oidc__PostLogoutRedirectUri=https://krint.example.com/
Oidc__RequireHttpsMetadata=true
Cors__AllowedOrigins__0=https://krint.example.com

When 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

bash
docker compose pull krint && docker compose up -d krint

Migrations 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
SymptomFix
Vault:MasterKey must decode to 32 bytesRegenerate with openssl rand -base64 32.
Vault:MasterKey does not match stored secretsKRINT 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 invalidOidc__Authority must match the IdP's issuer byte-for-byte.
401: signature key was not foundAPI 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 daemonThe /var/run/docker.sock bind is missing from the krint service.
<engine> container did not become ready on provisionKRINT 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 deniedRunning as non-root without socket access - keep the default root user or group_add the docker GID.
No free host port in rangekrint.yaml port_ranges exhausted - delete an instance or widen the range.
DB auth fails after changing the passwordPOSTGRES_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 DBThe 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 migrationsPostgres 15+ dropped the implicit CREATE grant. Make your app's role the owner of its database.

See also: Declarative instances · Nodes · Developer setup

Made with care by PianoNic.