Self-host
OrthID is open core. Run the same identity plane we operate, inside your own cloud and region, so identity data and signing keys never leave infrastructure you control.
The self-hosted distribution ships as a set of container images plus a Helm chart. You bring three things from your own environment: a Postgres database with row-level security, object storage, and a secrets backend. OrthID runs on top of them. Nothing phones home, and no identity record is sent to a third party.
This guide gets a single instance running locally with Docker Compose, then moves the same configuration to a production Kubernetes cluster with Helm. For multi-region topology and sizing, read Deploy.
Prerequisites
You provision and own each of these. OrthID connects to them at startup.
- PostgreSQL 15 or newer with row-level security enabled. OrthID isolates every tenant with RLS policies, so the database role it connects as must not be a superuser and must not have
BYPASSRLS. Provide a connection string and a dedicated, least-privilege role. - Object storage that speaks the S3 API (AWS S3, GCS in interop mode, or MinIO for local). Used for audit-log archives, exports, and large attachments. Identity records themselves live in Postgres, not object storage.
- A secrets backend for signing keys and the data-encryption key. Supported providers are HashiCorp Vault, AWS KMS, and a PKCS#11 HSM. See BYOK to wire up customer-managed keys.
BYPASSRLS attribute and grant it only the OrthID schema.Run locally with Docker Compose
For development and evaluation, the Compose stack brings up the API, the migration job, Postgres, and MinIO together. It uses a development secrets provider that generates a throwaway signing key on first boot. Do not use this provider in production.
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: orthid
POSTGRES_PASSWORD: orthid
POSTGRES_DB: orthid
ports: ["5432:5432"]
volumes: ["pgdata:/var/lib/postgresql/data"]
storage:
image: minio/minio
command: server /data
environment:
MINIO_ROOT_USER: orthid
MINIO_ROOT_PASSWORD: orthid-secret
ports: ["9000:9000"]
volumes: ["miniodata:/data"]
migrate:
image: ghcr.io/orthid/orthid:1.8
command: ["orthid", "migrate", "up"]
environment:
ORTHID_DATABASE_URL: postgres://orthid:orthid@postgres:5432/orthid
depends_on: [postgres]
orthid:
image: ghcr.io/orthid/orthid:1.8
ports: ["8080:8080"]
environment:
ORTHID_REGION: au-syd-1
ORTHID_DATABASE_URL: postgres://orthid:orthid@postgres:5432/orthid
ORTHID_STORAGE_ENDPOINT: http://storage:9000
ORTHID_STORAGE_BUCKET: orthid
ORTHID_SECRETS_PROVIDER: dev
ORTHID_PUBLIC_URL: http://localhost:8080
depends_on: [migrate, storage]
volumes:
pgdata:
miniodata:Bring the stack up, then create the first operator. The instance is ready when the health endpoint returns ok.
docker compose up -d # Wait for the API, then bootstrap the first operator account. docker compose exec orthid orthid bootstrap \ --email you@example.com \ --org "Acme Health" # http://localhost:8080 now serves the Operator console.
Required configuration
OrthID is configured entirely through environment variables (or a mounted orthid.yaml). These are the values every deployment must set. Secrets-provider settings are covered in BYOK.
# Home region for this instance. Identity data never leaves it. ORTHID_REGION=au-syd-1 # Postgres connection (role must NOT have BYPASSRLS). ORTHID_DATABASE_URL=postgres://orthid_app:***@db.internal:5432/orthid # S3-compatible object storage. ORTHID_STORAGE_ENDPOINT=https://s3.ap-southeast-2.amazonaws.com ORTHID_STORAGE_BUCKET=orthid-prod-au # Secrets backend: vault | aws-kms | hsm | dev (dev is local-only). ORTHID_SECRETS_PROVIDER=vault ORTHID_VAULT_ADDR=https://vault.internal:8200 ORTHID_VAULT_KEY=orthid/signing # Public URL the API and consoles are reached at. ORTHID_PUBLIC_URL=https://id.acme.health
Deploy to production with Helm
For production, use the Helm chart. It runs the API as a horizontally scalable deployment, runs database migrations as a pre-upgrade hook, and reads sensitive values from a Kubernetes secret rather than plain environment variables.
helm repo add orthid https://charts.orthid.dev helm repo update # Sensitive values come from a Secret, not the values file. kubectl create secret generic orthid \ --from-literal=databaseUrl="postgres://orthid_app:***@db.internal:5432/orthid" helm install orthid orthid/orthid \ --namespace orthid --create-namespace \ --set region=au-syd-1 \ --set publicUrl=https://id.acme.health \ --set secrets.provider=vault \ --set existingSecret=orthid \ --version 1.8.0
helm upgrade applies schema changes safely. For the full production values file, sizing guidance, and TLS setup, see Deploy.Health checks
OrthID exposes liveness and readiness probes. /healthz reports that the process is alive; /readyz reports that Postgres, object storage, and the secrets backend are all reachable and that signing keys have loaded. Point your load balancer and Kubernetes probes at /readyz, never at /healthz, so traffic only routes to instances that can actually serve requests.
# Liveness: is the process up?
curl -s https://id.acme.health/healthz
# {"status":"ok"}
# Readiness: are all dependencies connected and keys loaded?
curl -s https://id.acme.health/readyz | jq
# {
# "status": "ready",
# "region": "au-syd-1",
# "checks": { "database": "ok", "storage": "ok", "secrets": "ok" }
# }