Browse the docs
Guides

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.
Row-level security is mandatory
OrthID will refuse to start if it detects that its database role can bypass RLS. Tenant isolation depends on it. Create a role with no 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.

docker-compose.yml
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.

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

.env
# 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.

Terminal
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
Note
The chart runs migrations as a Helm pre-upgrade hook before rolling new pods, so a 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.

Terminal
# 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" }
# }

Next steps

  • BYOK to encrypt identity data with keys you hold in Vault, KMS, or an HSM.
  • Deploy for production topology, Helm values, sizing, TLS, and post-deploy verification.