Running Self-Hosted Keyright Across Multiple Nodes for High Availability
Your license server sits on your customers' activation hot path, so it should not be a single point of failure. Self-hosted Keyright is a stateless ASP.NET Core service — all state lives in your PostgreSQL — so high availability is just running two or three replicas behind a load balancer with no session affinity. Here is exactly how.
A license server has an unglamorous but unforgiving job: it sits on your customers’ activation path, and when it is unreachable at the wrong moment, someone cannot start the software they paid for. A good client design softens this with offline grace — a cached lease carries a brief outage — but “brief” is doing a lot of work in that sentence. If your single license node is down for an afternoon of patching, or its VM is being migrated, or it simply fell over, you are relying on every customer’s grace window being longer than your outage. That is not a plan. High availability is.
The good news is that making self-hosted Keyright highly available is almost anticlimactic, because the app was built for it. This post walks through exactly why, and how to stand up a two- or three-node deployment.
Key takeaway: Self-hosted Keyright is a stateless ASP.NET Core service — all state lives in your PostgreSQL — so high availability is just running two or three replicas behind a load balancer with no session affinity. The real thing to make highly available is Postgres. Multi-node HA is a Pro or Enterprise deployment (both cover up to three production nodes); Standard covers a single node.
Why a license server needs HA in the first place
Think about where the service actually sits. Every fresh activation, every seat claim, every online re-check of a lease goes through it. For most of your customers that traffic is bursty and invisible — right up until the moment it is not, because someone is onboarding a new machine, or a lease expired and the client is renewing. If the server is down then, that specific person is blocked, and blocked-at-activation is the worst possible time to be blocked. It turns into a support ticket in minutes.
You already mitigate the common case with offline validation between activations and downtime grace windows. HA is the other half: instead of hoping an outage stays inside everyone’s grace, you remove the single point of failure so there is no outage to grace over. Two nodes mean one can die, get patched, or get rescheduled while the other keeps answering.
The architecture that makes this easy
Self-hosted Keyright has exactly two moving parts: a stateless app, and your database.
The app is the ghcr.io/delta1-labs/keyright image — one ASP.NET Core service listening on port 8080. The image is app-only: there is no database inside it. It keeps no state between requests. Licenses, activations, seats, credits, the audit log, the signing keys — all of it lives in PostgreSQL, which you point the app at with the KEYRIGHT_DB connection string. The app creates and migrates its own schema on first start, so there is no SQL to run by hand.
That one design decision — stateless app, all state in your database — is what makes HA trivial. Because nothing is pinned to a particular process, you can run as many copies of the app as you like, and any copy can answer any request. There is no in-memory session, no node-local cache of truth, nothing to lose when a node goes away. Scaling the app out is just running the same container more than once.
Running multiple replicas
If you are on Docker Compose, scaling the app tier is a flag:
docker compose up -d --scale keyright=3
That gives you three identical app containers, all reading the same KEYRIGHT_DB and all carrying the same configuration. Put a load balancer in front (below) and you have a three-node deployment.
On Kubernetes with the Helm chart, it is a value:
# values.yaml
replicaCount: 3
helm upgrade --install keyright delta1/keyright -f values.yaml
Kubernetes schedules three pods, the Service load-balances across them, and because the pods are stateless you can add or drain them freely. Spread them across nodes or availability zones with pod anti-affinity if you want to survive the loss of a whole machine or zone.
One thing stays identical across every replica: the self-license key. Keyright is fail-closed — it needs a valid self-license (or trial) key to serve traffic at all — and every node runs with the same key. There is no per-node registration or node-counting handshake; a renewal or an edition change is just swapping the key in your config and restarting the nodes. Rolling the key out is a configuration change, not a licensing dance.
The load balancer and the health probes
Put any HTTP load balancer in front of the app nodes — your cloud’s L7 balancer, an ingress controller, nginx, HAProxy, whatever you already run. Two things to get right:
No session affinity. Do not enable sticky sessions. Because the app is stateless and clients validate offline between activations, there is nothing stuck to a node that affinity would need to preserve. Send any request to any node. Turning on affinity only concentrates load and defeats the point. Concurrency across nodes is safe without it — seats and credits use a serializable check-and-deduct in Postgres, so two nodes physically cannot double-spend the same seat or balance.
Health probes. Keyright exposes two endpoints for exactly this:
GET /health— liveness. A static200that never fails a node out over a transient database blip. Use it to decide whether the process is alive.GET /health/ready— readiness. This one also checks database connectivity and returns503when Postgres is unreachable. Use it to decide whether a node should receive traffic.
A typical balancer probe:
health check: GET /health/ready interval 10s healthy 2 unhealthy 3
Terminate TLS at the balancer and forward X-Forwarded-For (the real client IP) and X-Forwarded-Proto (so the dashboard and portal generate https URLs) — the app honours both. Expose only the public :443 through the balancer; keep /admin/* and /internal/* reachable only from inside your network. Your customers only ever need the /v1/* path.
PostgreSQL is the real bottleneck
Here is the part people get backwards. Scaling the app is nearly free, so it is not where your attention should go. The thing that has to be reliable — and the thing that actually limits how far you scale — is PostgreSQL, because that is where every byte of state lives.
So invest there. For anything past evaluation, do not run the bundled database the kits offer for convenience; point KEYRIGHT_DB at a proper managed or highly available Postgres — a primary with a standby, automated failover, and real backups. A stateless app in front of a single fragile database is not highly available; you have just moved the single point of failure one layer down. Make the database the HA component and the app tier follows for free. At very large scale (into the millions of active licenses) add PgBouncer for connection pooling and give Postgres more cores — the scaling guidance in the operations docs has concrete sizing per tier.
The reason the app can get away with being so light is the shape of the workload. The hot path — /v1/activate and /v1/validate — is a signature check plus a seat count, and clients cache a signed lease and validate offline between activations. You are not taking an HTTP request on every app launch; you take one at activation and then periodically when a lease renews. That is a small, bursty, CPU-light load, which is exactly why two or three modest nodes comfortably cover a deployment with hundreds of thousands of active licenses.
A free bonus: zero-downtime upgrades
Once you are running 2+ nodes, upgrades stop being an outage. Because the app is backward-compatible within a minor line and migrations apply automatically on startup, you can roll one node at a time: drain it, pull the new image, let it come back and go ready, then move to the next. The load balancer keeps serving from the healthy nodes throughout. The same property that gives you HA against failure gives you HA against maintenance — which, in practice, is the outage you actually schedule most often.
Which edition you need
Self-hosted Keyright is priced per deployment, and the editions differ on node count:
- Standard covers a single production node — fine for a straightforward deployment, but by definition not highly available.
- Pro and Enterprise both cover up to three production nodes — which is what multi-node HA requires.
Standard and Pro run the exact same feature engine; node count is the only thing that separates them, so “I want HA” is simply a reason to be on Pro rather than Standard. Enterprise adds SSO, white-label branding, and air-gapped credit blocks on top of the same three-node allowance. Node counts are a contractual term — the software does not count or enforce nodes, and the same self-license key runs on every replica. See the pricing for the current numbers.
Where Keyright fits
Because Keyright is one product in both deployment modes, the stateless-app-plus-your-database architecture is the same whether you are evaluating on a laptop or running three nodes in production — nothing about your integration changes as you scale out. If you are still deciding whether to self-host at all, the self-hosted vs cloud trade-offs lays out the decision, and if you are moving off a homegrown system, migrating without breaking customers is the playbook. When you are ready to stand up HA, the operations guide has the backup, upgrade, and sizing detail that turns two running nodes into a production deployment you can sleep behind.
Try Nebula.NET
Harden your .NET code in minutes — start with the free edition.