Self-hosting
Deployment guide (step by step)
A step-by-step walkthrough to deploy self-hosted Keyright into your own environment: prerequisites, get the deploy kit, generate secrets, prepare PostgreSQL, choose a deploy path (Docker Compose, Kubernetes/Helm, or Windows/IIS), install your license, verify, and sign in — from zero to a running, licensed instance.
This is the linear, do-this-then-that guide to a running instance. It assumes you’ve skimmed the architecture overview. Every setting it touches has a full reference in Configuration; when a step needs more depth it links there.
By the end you’ll have a licensed Keyright instance answering /health, a signed-in dashboard, and the exact base URL your product’s SDK will point at.
Step 0 — Check the prerequisites
- A container runtime (recommended): Docker 24+ with Compose v2, or Kubernetes 1.24+ with Helm 3. Or Windows/IIS with the .NET 8 Hosting Bundle (Step 4C, no containers).
- PostgreSQL 14+ (16 recommended), UTF-8. For a first evaluation you can let the bundle run one for you (Step 3); for production, prepare your own managed/HA Postgres.
- A Keyright license or trial key from Delta1 — required to run. Contact sales for a 30-day Enterprise trial or a paid Standard/Enterprise license. Save the key you receive as
license.json. opensslon the machine you run these steps from (for generating secrets in Step 2).
Step 1 — Get the deploy kit
The container image is public — pull it with no auth (pin the version for a reproducible install):
docker pull ghcr.io/delta1-labs/keyright:2.2.2 # or :latest
The deploy kit — the config files the rest of this guide references — is sent to you by Delta1 with your license (or on request from sales). It contains:
deploy/docker-compose.yml+deploy/.env.example(Docker Compose)deploy/helm/keyright/(Kubernetes / Helm)deploy/windows/(Windows / IIS)
Unpack it into a working directory; the commands below are run from there.
Step 2 — Generate your secrets
Keyright needs three secrets you create and keep. Generate them now:
openssl rand -hex 24 # KEYRIGHT_ADMIN_TOKEN (your root admin credential)
openssl rand -base64 32 # KEYRIGHT_KEK (encrypts signing keys at rest — 32 bytes)
openssl genrsa 2048 | openssl pkcs8 -topk8 -nocrypt -outform DER | base64 -w0 # KEYRIGHT_SIGNING_KEY (optional; signs offline leases)
(On macOS/BSD, base64 has no -w0; use ... -outform DER | base64 | tr -d '\n'.)
Back up the KEK and signing key separately and securely. They’re yours — we never see them — and without the KEK, the encrypted signing keys in your database cannot be recovered. The signing key is optional (Keyright generates a per-tenant one in the DB if you skip it), but if you set it, back it up too.
Full meaning of each variable is in Configuration.
Step 3 — Prepare the database
You provide a PostgreSQL server and an empty database; Keyright builds the schema itself on first start — there is no .sql file and no SQL to run by hand. Migrations are compiled into the image and applied automatically on every boot (db.Database.Migrate()).
Production — your own Postgres. Create an empty database and a login role, and make the role the database owner (the simplest correct setup — the app must be able to run CREATE/ALTER/DROP to apply migrations):
CREATE DATABASE keyright ENCODING 'UTF8' TEMPLATE template0;
CREATE ROLE keyright_app LOGIN PASSWORD 'a-strong-password';
ALTER DATABASE keyright OWNER TO keyright_app;
On PostgreSQL 15+, if the role is not the owner, also GRANT ALL ON SCHEMA public TO keyright_app;. Some managed providers (Azure Flexible Server, Cloud SQL) don’t allow CREATE DATABASE over SQL — create the empty database + role from their console/CLI, then grant as above. AWS RDS/Aurora, Cloud SQL, Azure, Neon, Supabase all work. Then build your connection string (Npgsql format, TLS required):
Host=db.internal.example.com;Port=5432;Database=keyright;Username=keyright_app;Password=...;SSL Mode=Require;Trust Server Certificate=true
That string is your KEYRIGHT_DB. See Configuration → Database for TLS options and managed-provider notes.
Evaluation — skip this step. Docker Compose and the Helm chart can start a postgres:16 for you with the database and role pre-created. Fine for a first run and small single-node installs; move to your own managed/HA Postgres for production.
Step 4 — Deploy
Pick the path that matches your environment. All three produce the same running service.
Step 4A — Docker Compose (single node)
The fastest way to a running instance — Postgres + the app on one host:
cp deploy/.env.example deploy/.env # then fill in the secrets from Step 2 (and KEYRIGHT_DB for production)
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d
Set your license in deploy/.env as KEYRIGHT_SELF_LICENSE (the license JSON on one line) or mount a file and set KEYRIGHT_SELF_LICENSE_FILE (the kit has a commented volumes: example). Pin a version with KEYRIGHT_VERSION. For production set a real KEYRIGHT_DB rather than the bundled Postgres.
Step 4B — Kubernetes (Helm)
Save your license to license.json and pass it with --set-file — do not use --set/--set-string for the license or the DB string: both contain commas/braces that Helm’s --set parser splits on, which fails with key "…" has no value.
Production — your own external Postgres (recommended):
helm upgrade --install keyright deploy/helm/keyright -n keyright --create-namespace \
--set image.repository=ghcr.io/delta1-labs/keyright \
--set-string secrets.adminToken="$KEYRIGHT_ADMIN_TOKEN" \
--set-string secrets.kek="$KEYRIGHT_KEK" \
--set-file secrets.selfLicense=license.json \
--set postgres.enabled=false \
--set-string externalDatabase.connectionString="Host=db.example.com;Port=5432;Database=keyright;Username=keyright_app;Password=...;SSL Mode=Require;Trust Server Certificate=true" \
--set replicaCount=2
Evaluation — bundled in-cluster Postgres (drop the two DB lines above and keep the default):
helm upgrade --install keyright deploy/helm/keyright -n keyright --create-namespace \
--set image.repository=ghcr.io/delta1-labs/keyright \
--set image.tag=2.2.2 \
--set-string secrets.adminToken="$KEYRIGHT_ADMIN_TOKEN" \
--set-string secrets.kek="$KEYRIGHT_KEK" \
--set-file secrets.selfLicense=license.json
Pin the image version. Add --set image.tag=<version> (e.g. --set image.tag=2.2.2) for a reproducible deploy. Unset, it defaults to the chart’s appVersion.
Docker Hub rate limits? The bundled Postgres pulls postgres:16 from Docker Hub, which rate-limits anonymous pulls. If the postgres pod is stuck ImagePullBackOff, point it at your own registry/mirror with --set postgres.image=<registry>/postgres:16 (see Configuration → Kubernetes values).
Expect a few restarts on the first bundled-Postgres boot. Keyright runs its migrations at startup, and the bundled Postgres takes a moment to become Ready — so on a fresh evaluation install the app pod may
CrashLoopBackOffa few times while it waits for the DB. This is expected, not an error; it clears within a minute or two once Postgres is Ready. With an external Postgres, a persistent crash loop instead means the DB genuinely isn’t reachable (see Operations → Troubleshooting).
secrets.adminToken and secrets.kek are required — the chart refuses to render without them. See Configuration → Kubernetes for ingress, replicas, resources, white-label, the bundled-image override, and reaching the service with a port-forward.
Step 4C — Windows / IIS
For shops that won’t run containers:
pwsh deploy/windows/build-win-bundle.ps1 # add -SelfContained if the server has no .NET 8
Unzip .artifacts/keyright-win/keyright-issuing-win-x64.zip into an IIS site’s physical path. The bundled web.config wires the ASP.NET Core Module (in-process). Set the KEYRIGHT_* values as machine environment variables (preferred) or in web.config, install the .NET 8 Hosting Bundle, and start the site. Point KEYRIGHT_DB at your PostgreSQL.
Step 5 — Verify
curl -fsS http://localhost:8080/health # {"status":"ok","service":"keyright-issuing","version":"2.2.2"}
curl -H "X-Admin-Token: $KEYRIGHT_ADMIN_TOKEN" http://localhost:8080/admin/self-license
/health is liveness; /health/ready also checks the database. /admin/self-license reports your edition, phase (active / expiring / grace / gated), days remaining, and whether the runtime is blocked — if it says gated, your license didn’t load (see Licensing).
Your base URL. For a local/evaluation run it’s http://localhost:<KEYRIGHT_PORT> (default http://localhost:8080); in production it’s whatever host your load balancer serves (e.g. https://licensing.acme.example). Every curl and every SDK ServiceUrl from here on uses that base.
On Kubernetes without an ingress, reach the service with a port-forward. The service is named <release>-keyright — for the release keyright used above that’s keyright-keyright:
kubectl port-forward -n keyright svc/keyright-keyright 8080:8080
Step 6 — Sign in to the dashboard
Open <base>/dashboard. On a fresh instance you have no dashboard user yet — sign in with your admin token: expand “Advanced: use an access token”, paste your KEYRIGHT_ADMIN_TOKEN, then View dashboard. (Email/password sign-in needs a user provisioned via POST /admin/team/{id}/set-password; SSO is the Enterprise KEYRIGHT_SSO_* option.) The admin token is your root credential — treat it accordingly.
You’re running — what’s next
- Wire your product to your instance — create a product, generate its signing key, point your app’s SDK at your base URL, and issue your first license. This is the step that makes self-hosting real.
- Licensing & editions — how your own key gates the instance, the trial, and renewals.
- Operations & troubleshooting — scaling, backups, upgrades, air-gapped, and the fix for anything that didn’t come up green above.
Stuck on a step? Talk to sales/support — we’ll get you running.