{"id":51385584,"url":"https://github.com/korjavin/ory-compose","last_synced_at":"2026-07-03T20:02:09.044Z","repository":{"id":358278265,"uuid":"1240284370","full_name":"korjavin/ory-compose","owner":"korjavin","description":"Lightweight self-hosted Ory stack — Kratos (identity + social sign-in) + Hydra (OAuth2/OIDC for your apps) + Login UI, on SQLite. Git-ops deploy via Portainer + GHCR-vendored images.","archived":false,"fork":false,"pushed_at":"2026-06-29T05:39:48.000Z","size":96,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-29T07:17:47.895Z","etag":null,"topics":["docker-compose","hydra","identity-management","kratos","oauth2","oidc","ory","portainer","self-hosted","sqlite"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/korjavin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-16T00:51:40.000Z","updated_at":"2026-05-16T19:26:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/korjavin/ory-compose","commit_stats":null,"previous_names":["korjavin/ory-compose"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/korjavin/ory-compose","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/korjavin%2Fory-compose","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/korjavin%2Fory-compose/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/korjavin%2Fory-compose/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/korjavin%2Fory-compose/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/korjavin","download_url":"https://codeload.github.com/korjavin/ory-compose/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/korjavin%2Fory-compose/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35099548,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-03T02:00:05.635Z","response_time":110,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker-compose","hydra","identity-management","kratos","oauth2","oidc","ory","portainer","self-hosted","sqlite"],"created_at":"2026-07-03T20:02:08.136Z","updated_at":"2026-07-03T20:02:09.027Z","avatar_url":"https://github.com/korjavin.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Ory Stack (Kratos + Hydra + Login UI) for Portainer\n\nA git-ops Docker Compose deployment that gives you:\n\n- **Ory Kratos** — identity management. Sign-in surface is locked to **OIDC providers** (Google, GitHub, GitLab, Pocket-ID) and **passkeys** (WebAuthn passwordless). Password and email-magic-code login are off by default. TOTP and WebAuthn-2FA can be layered on top.\n- **Ory Hydra** — OAuth2 / OIDC provider you point your other apps at (Outline, Forgejo, etc.). One Hydra client per app.\n- **kratos-selfservice-ui-node** — reference Login / Registration / Settings / Recovery UI.\n- **Custom consent service** (`consent/`) — replaces the Login UI's consent handler. Enforces per-client `required_groups` and copies `groups` into ID + access tokens, so each app's access is gated centrally.\n- **Invite CLI** (`invite/`) — pre-creates a Kratos identity with the right group memberships and prints a 1h recovery link. Send the link, recipient picks whichever auth method(s) they want and links them all to the same identity.\n\nBoth Kratos and Hydra run on **SQLite** with data persisted in named Docker volumes — lightweight, no Postgres dependency.\n\n## Architecture\n\n```\n       Browser\n          │\n          ▼\n  ┌──────────────────────────────────────────────────┐\n  │           auth.example.com (one origin)          │\n  │  ┌──────────────┐  ┌──────────────┐  ┌────────┐  │\n  │  │   Login UI   │  │   Kratos     │  │Consent │  │\n  │  │   (catch-all)│  │   (public,   │  │service │  │\n  │  │              │  │   path-routed│  │/consent│  │\n  │  │  /login etc. │  │  /self-      │  │        │  │\n  │  │              │  │   service/*) │  │        │  │\n  │  └──────────────┘  └──────┬───────┘  └────────┘  │\n  └─────────────────────────────│────────────────────┘\n                                │ identity\n                                ▼\n                       ┌──────────────────┐\n                       │  Hydra (public)  │\n                       │ hydra.example.com│\n                       └────────┬─────────┘\n                                │ OIDC discovery + tokens\n                                ▼\n                Your apps (Outline, Forgejo, …) — configure them\n                with hydra.example.com/.well-known/openid-configuration\n```\n\nTrust boundaries:\n\n| Endpoint                   | Network           | Exposure                       |\n|----------------------------|-------------------|--------------------------------|\n| Login UI      (`:3000`)    | traefik + internal| `https://${LOGIN_UI_HOST}` (catch-all paths) |\n| Kratos public (`:4433`)    | traefik + internal| `https://${LOGIN_UI_HOST}/self-service/…`, `/.well-known/…`, `/sessions/…`, `/schemas/…`, `/health/…` (path-routed, priority 90) |\n| Consent       (`:3001`)    | traefik + internal| `https://${LOGIN_UI_HOST}/consent` (priority 100) |\n| Kratos admin  (`:4434`)    | internal only     | never on the public internet   |\n| Hydra public  (`:4444`)    | traefik + internal| `https://${HYDRA_PUBLIC_HOST}` |\n| Hydra admin   (`:4445`)    | internal only     | never on the public internet   |\n\nLogin UI, Kratos public, and Consent all live on the **same origin** (`LOGIN_UI_HOST`) so CSRF and session cookies just work. Kratos's per-flow CSRF cookies don't honor `serve.public.cookies.domain` in v1.3.x, so a separate-subdomain layout produces an infinite redirect loop on every flow — same-origin sidesteps the bug entirely.\n\nAdmin APIs are reachable from other containers on the `ory_internal` Docker network. To talk to them from your laptop, use `docker exec ory-hydra hydra ...` or open a temporary SSH tunnel.\n\n## Files\n\n```\n.\n├── docker-compose.yml\n├── .env.example\n├── config/                           # built into ory-kratos-config image\n│   ├── Dockerfile                    # alpine + gettext, baked config files\n│   ├── render.sh                     # entrypoint: envsubst + copy\n│   └── kratos/\n│       ├── kratos.yml.tmpl           # rendered at startup with envsubst\n│       ├── identity.schema.json      # user shape (incl. groups[])\n│       ├── oidc.google.jsonnet       # Google → Kratos identity mapper\n│       ├── oidc.pocket-id.jsonnet\n│       ├── oidc.github.jsonnet\n│       └── oidc.gitlab.jsonnet\n├── consent/                          # our Go consent service\n│   ├── main.go\n│   ├── go.mod\n│   └── Dockerfile                    # → ghcr.io/\u003cowner\u003e/ory-consent:latest\n├── invite/                           # our Go invite CLI\n│   ├── main.go\n│   ├── go.mod\n│   └── Dockerfile                    # → ghcr.io/\u003cowner\u003e/ory-invite:latest\n└── .github/\n    ├── scripts/\n    │   └── build-deploy-branch.sh    # shared: rebuild deploy from master + tag pins\n    └── workflows/\n        ├── deploy.yml                # master push (non-image) → deploy branch\n        ├── vendor-images.yml         # weekly: pull oryd/* → push vendored to GHCR\n        └── build-services.yml        # build \u0026 push consent + invite + kratos-config\n```\n\n## How deploys are pinned\n\n`master` always references images as `:latest` (e.g. `ghcr.io/korjavin/ory-consent:latest`). The `deploy` branch — what Portainer actually pulls — is auto-generated with each image pinned to a concrete tag.\n\n| Image | Pinned to |\n|---|---|\n| `ory-consent`, `ory-invite`, `ory-kratos-config` | the **master commit SHA** that last built it (built by `build-services.yml`) |\n| `kratos-vendor`, `hydra-vendor`, `kratos-selfservice-ui-node-vendor` | `d-\u003cfirst-12-of-upstream-digest\u003e` (built by `vendor-images.yml`) |\n\nEach deploy-branch commit also carries `image-tags.env`, recording the exact tags for that revision. To inspect what's currently deployed:\n\n```bash\ngit show origin/deploy:image-tags.env\n```\n\nTo roll back one image (e.g. revert ory-consent to its previous SHA):\n\n```bash\n# Find the SHA you want to roll back to\ngit log --oneline master -- consent/\n# Force the deploy branch to that pinned tag\nORY_CONSENT_TAG=\u003csha\u003e bash .github/scripts/build-deploy-branch.sh\n```\n\nOr just `git revert` the offending master commit; the next `Deploy Ory Stack` run repins automatically.\n\nThe pinning model means Portainer always sees a tag it hasn't pulled before → it pulls every redeploy → no more \"Portainer cached `:latest`\" surprises.\n\n## How environment variables are wired\n\nYou said you don't want a `.env` file on the server — Portainer holds the values. The compose file references env names with sensible defaults; only **hostnames** and **secrets** truly need to be set in Portainer's stack-env panel.\n\nKratos itself doesn't natively read `${VAR}` from its YAML config. We work around that with a tiny init container `kratos-config` built from `./config/`. The image bakes in `gettext` (for `envsubst`), the `kratos.yml.tmpl` template, all OIDC mapper jsonnets, and the identity schema. On every restart its entrypoint renders the template against the current Portainer env vars and drops the result into a shared volume that Kratos mounts read-only at `/etc/config/kratos`.\n\nEdit-and-deploy loop:\n\n1. Edit `config/kratos/kratos.yml.tmpl` (or any other file in `config/`) on master.\n2. The `Build Custom Services` workflow rebuilds `ghcr.io/\u003cowner\u003e/ory-kratos-config:latest` and force-pushes the `deploy` branch.\n3. Portainer pulls the new image on redeploy; the next `kratos-config` run renders the updated template.\n\n## Required secrets\n\n| Variable                       | Purpose                                | Generator                              |\n|--------------------------------|----------------------------------------|----------------------------------------|\n| `KRATOS_COOKIE_SECRET`         | signs Kratos session cookies           | `openssl rand -hex 32`                 |\n| `KRATOS_CIPHER_SECRET`         | encrypts secrets at rest in Kratos     | **`openssl rand -hex 16`** (must be exactly 32 chars — xchacha20-poly1305 key length) |\n| `HYDRA_SECRETS_SYSTEM`         | encrypts Hydra DB rows                 | `openssl rand -hex 32`                 |\n| `HYDRA_SECRETS_COOKIE`         | signs Hydra cookies                    | `openssl rand -hex 32`                 |\n| `HYDRA_PAIRWISE_SALT`          | salt for pairwise OIDC subject IDs     | `openssl rand -hex 32`                 |\n| `LOGIN_UI_COOKIE_SECRET`       | signs Login UI cookies                 | `openssl rand -hex 32`                 |\n| `LOGIN_UI_CSRF_COOKIE_SECRET`  | signs Login UI CSRF cookies            | `openssl rand -hex 32`                 |\n\n## DNS \u0026 TLS\n\nSet up **two** DNS A/AAAA records pointing to your Hetzner host:\n\n```\nauth.example.com    → host    (Login UI + Kratos public + Consent, all same-origin)\nhydra.example.com   → host    (Hydra public OAuth2/OIDC)\n```\n\nTraefik handles certs via the `myresolver` (or whatever you set in `TRAEFIK_CERTRESOLVER`). The cookie domain must be a parent that covers all three — set `COOKIE_DOMAIN=.example.com`.\n\n## Setting up the social providers\n\nThe redirect URI in every provider's console is always:\n\n```\nhttps://${LOGIN_UI_HOST}/self-service/methods/oidc/callback/\u003cprovider-id\u003e\n```\n\n`\u003cprovider-id\u003e` is `google`, `pocket-id`, `github`, or `gitlab`. Kratos rejects providers with null/empty `client_id` or `client_secret` at startup, so every configured provider must have valid credentials. To temporarily disable one, remove its block from `config/kratos/kratos.yml.tmpl` (and the corresponding env vars from `docker-compose.yml`'s `kratos-config` env block).\n\n| Provider | Console | Notes |\n|---|---|---|\n| **Google** | \u003chttps://console.cloud.google.com/apis/credentials\u003e → OAuth client (Web) | Set `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`. |\n| **Pocket-ID** | Your Pocket-ID admin UI → create OIDC client | Set `POCKET_ID_ISSUER_URL` to your Pocket-ID base URL (must serve `.well-known/openid-configuration`), plus client id/secret. |\n| **GitHub** | \u003chttps://github.com/settings/developers\u003e → New OAuth App | Set `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`. GitHub doesn't issue `email_verified`; we trust the email returned by `user:email` scope. |\n| **GitLab** | \u003chttps://gitlab.com/-/profile/applications\u003e (or your self-hosted instance) | Set `GITLAB_ISSUER_URL`, `GITLAB_CLIENT_ID`, `GITLAB_CLIENT_SECRET`. Scopes: `openid profile email`. |\n\nTo add another provider (Microsoft/Entra, Apple, Discord, …), add an entry under `selfservice.methods.oidc.config.providers` in `config/kratos/kratos.yml.tmpl`, ship a matching mapper jsonnet under `config/kratos/`, list it in `config/render.sh`, and add the env vars to the `kratos-config` init container in `docker-compose.yml`.\n\n### Passkeys (WebAuthn)\n\nBoth passkey-passwordless and WebAuthn-2FA methods are enabled by default. `WEBAUTHN_RP_ID` must be a registrable suffix of every origin where you'll use passkeys — typically your cookie domain *without* the leading dot (e.g. `example.com` when `COOKIE_DOMAIN=.example.com`). Once a user logs in and clicks \"Add passkey\" in the settings UI, they can use it for subsequent logins on any subdomain under `WEBAUTHN_RP_ID`.\n\n## Invitations: pre-create identities + send a 1h link\n\nThe default sign-in surface is OIDC + passkey only — no password or email-magic-code login. So practically the only way someone gets in is either (a) you invite them, or (b) they already have a Google/GitHub/etc. account that maps to an existing Kratos identity (which doesn't happen by default — see below). Either way, invitation is the primary onboarding path.\n\nUse the `invite` CLI. It hits Kratos's admin API on the internal Docker network — run it on the host:\n\n```bash\ndocker run --rm --network=ory_internal \\\n  -e KRATOS_ADMIN_URL=http://kratos:4434 \\\n  ghcr.io/korjavin/ory-invite:latest \\\n  alice@example.com outline notan\n```\n\nWhat happens:\n\n1. The CLI creates a Kratos identity with `traits.email = alice@example.com` and `metadata_admin.groups = [\"outline-users\", \"notan-users\"]`. Groups deliberately live under `metadata_admin` (not `traits`) so the user can NOT self-edit them via the Settings page.\n2. It generates a Kratos recovery link valid for `KRATOS_RECOVERY_LIFESPAN` (default 1h).\n3. It prints the link.\n\nYou forward the link to Alice (Telegram, email, however). When she clicks it:\n\n1. Kratos validates the recovery token → creates a session.\n2. She lands on `/settings`, where she can pick any combination of: link Google / GitHub / GitLab / Microsoft / Pocket-ID, add a passkey, set a password.\n3. All of those credentials attach to the **same** identity, with `groups = [\"outline-users\", \"notan-users\"]` already set. Subsequent logins via any of those methods land on the same record.\n\nIf she doesn't click within an hour, the link expires — re-run the `invite` command to mint a fresh one (the identity is still there, just dormant).\n\nFlags:\n\n```\ninvite [flags] \u003cemail\u003e \u003capp1\u003e [app2 ...]\n  --expires-in 1h         # how long the link is valid\n  --first \"Alice\"         # optional first name\n  --last \"Doe\"            # optional last name\n  --extra-groups admins   # additional groups beyond \u003capp\u003e-users\n```\n\n## Registering your apps as Hydra OAuth2 clients\n\nThere is no Hydra admin UI — you create clients via its admin API. Easiest way is to `docker exec` into the Hydra container and use the bundled CLI.\n\nEach app gets its own Hydra client with `metadata.required_groups` listing the Kratos groups whose members may use it. The custom consent service enforces this: anyone not in at least one of those groups is rejected at consent time, before they ever see the app.\n\nExample: Outline, gated on `outline-users`:\n\n```bash\ndocker exec -it ory-hydra hydra create client \\\n  --endpoint http://localhost:4445 \\\n  --name \"Outline\" \\\n  --grant-type authorization_code,refresh_token \\\n  --response-type code,id_token \\\n  --scope openid,offline,profile,email,groups \\\n  --redirect-uri https://outline.example.com/auth/oidc.callback \\\n  --token-endpoint-auth-method client_secret_basic \\\n  --metadata '{\"required_groups\":[\"outline-users\",\"admins\"]}'\n```\n\n\u003e Don't pass `--skip-consent`. The consent service must run on every request so it can enforce `required_groups` and inject `groups` into the token.\n\nHydra prints the generated `client_id` and `client_secret` — paste them into Outline's env vars.\n\nIn Outline (or any app), the OIDC discovery URL is:\n\n```\nhttps://hydra.example.com/.well-known/openid-configuration\n```\n\nOutline-specific env mapping:\n\n```\nOIDC_AUTH_URI=https://hydra.example.com/oauth2/auth\nOIDC_TOKEN_URI=https://hydra.example.com/oauth2/token\nOIDC_USERINFO_URI=https://hydra.example.com/userinfo\nOIDC_DISPLAY_NAME=Sign in\nOIDC_SCOPES=openid profile email\n```\n\n### Changing required_groups later\n\nYou can update a client's metadata without recreating it:\n\n```bash\ndocker exec -it ory-hydra hydra update client \u003cclient-id\u003e \\\n  --endpoint http://localhost:4445 \\\n  --metadata '{\"required_groups\":[\"outline-users\",\"admins\",\"editors\"]}'\n```\n\nChanges take effect on the next consent (i.e. the next time a user logs in fresh). Users who already have a refresh token keep working until it expires; revoke their tokens via `hydra revoke token` if you need an immediate cutoff.\n\n## The consent service in one paragraph\n\n`consent/` is a ~250-line Go service that owns the `/consent` URL on `auth.example.com`. On every consent request it:\n\n1. Asks Hydra for the consent challenge details (`/admin/oauth2/auth/requests/consent`).\n2. Asks Kratos for the identity (`/admin/identities/\u003csubject\u003e`) — pulls `metadata_admin.groups` (admin-only, not user-editable), plus `traits.email`, `traits.name`.\n3. Reads `client.metadata.required_groups`. If non-empty and the user is in none of them → reject. Otherwise → accept.\n4. On accept, copies `groups` into both `id_token.groups` and `access_token.groups`, plus standard email/name claims.\n\nAuto-accept (no consent screen) because every Hydra client is first-party (your apps). If you ever expose Hydra to third-party apps, add a confirmation page here.\n\n## Managing users \u0026 groups\n\nKratos has **no built-in admin UI**. For day-to-day work use the `invite` CLI above. For other operations, talk to the admin API on `:4434` (internal-only) directly:\n\n```bash\n# List identities\ndocker exec -it ory-kratos kratos list identities --endpoint http://localhost:4434\n\n# Change someone's groups (replaces the list).\n# Groups live under metadata_admin — admin-only, not user-editable from Settings.\ndocker exec -it ory-kratos kratos patch identity --endpoint http://localhost:4434 \\\n  \u003cid\u003e -p '[{\"op\":\"replace\",\"path\":\"/metadata_admin/groups\",\"value\":[\"admin\",\"outline-users\",\"forgejo-users\"]}]'\n\n# Revoke all of someone's sessions immediately (e.g. after offboarding)\ndocker exec -it ory-kratos kratos delete identity --endpoint http://localhost:4434 \u003cid\u003e\n```\n\nThe `groups` array is the only thing the consent service consults — change it and the next consent (after re-login or token refresh) reflects the new permissions. To force an immediate cutoff, also revoke their Hydra refresh tokens: `docker exec -it ory-hydra hydra revoke token --endpoint http://localhost:4445 \u003ctoken\u003e`.\n\nIf you want a clickable UI later, drop in a community admin tool — e.g. \u003chttps://github.com/dfoxg/kratos-admin-ui\u003e — pointed at the same internal Kratos admin URL. Keep it behind oauth2-proxy or your VPN; never expose the admin port publicly.\n\n### Group naming convention\n\nThe `invite` CLI assigns groups as `\u003capp\u003e-users` (e.g. `outline-users`, `notan-users`). Match that in each Hydra client's `metadata.required_groups`. You can also add cross-cutting groups (`admins`, `editors`) — `--extra-groups admins` on the invite CLI, then list `admins` in `required_groups` for any app admins should be able to use.\n\n## Deploy in Portainer\n\n1. **Create the Traefik network** if it doesn't exist:\n   ```bash\n   docker network create traefik_default\n   ```\n2. **Before first deploy, run the image-building workflows once manually:**\n   - `Vendor Ory Images to GHCR` — mirrors `oryd/kratos`, `oryd/hydra`, `oryd/kratos-selfservice-ui-node` to your GHCR namespace.\n   - `Build Custom Services` — builds `ory-consent`, `ory-invite`, and `ory-kratos-config` from this repo into GHCR.\n\n   After both succeed, five images exist under `ghcr.io/\u003cowner\u003e/`: `kratos-vendor`, `hydra-vendor`, `kratos-selfservice-ui-node-vendor`, `ory-consent`, `ory-kratos-config`. (`ory-invite` exists too but is only used via `docker run` on demand.)\n3. **Create the stack in Portainer** → \"Repository\" mode, point at this repo, branch `deploy`.\n4. **Paste the env vars** from `.env.example` into Portainer's env panel (replace placeholder values).\n5. Hit **Deploy**. Watch logs for `kratos-config`, then `kratos-migrate`, then `kratos`, `hydra-migrate`, `hydra`, `login-ui`, `consent` coming up in order.\n6. Set up the Portainer **redeploy webhook**, copy its URL into the GitHub repo as the secret `PORTAINER_REDEPLOY_HOOK`. From then on, every push to `master` triggers a redeploy via the `deploy` branch.\n\n## Vendoring images\n\n`.github/workflows/vendor-images.yml` runs weekly (Mondays 04:00 UTC) and:\n\n1. Pulls `oryd/kratos:v1.3.1`, `oryd/hydra:v2.3.0`, and `oryd/kratos-selfservice-ui-node:v1.3.1` from Docker Hub.\n2. Re-pushes them as `ghcr.io/\u003cowner\u003e/\u003cname\u003e-vendor:latest`.\n3. Logs the upstream digest, force-pushes the `deploy` branch, and pings the Portainer webhook.\n\nBump the upstream tags in `.github/workflows/vendor-images.yml` when new Ory releases come out, then run the workflow manually with **Run workflow**.\n\n## First-boot smoke test\n\n```bash\n# 1. Discovery doc (public)\ncurl -s https://hydra.example.com/.well-known/openid-configuration | jq .issuer\n\n# 2. Kratos health\ncurl -s https://auth.example.com/health/ready\n\n# 3. Browser: visit https://auth.example.com/  → log in via Google or Pocket-ID\n```\n\n## Switching to Postgres later\n\nBoth Kratos and Hydra accept a `DSN` env var. Change `KRATOS_DSN` and `HYDRA_DSN` to a `postgres://...` URL, add a Postgres service to the compose file (or point at an existing one), and restart. The `*-migrate` containers will run any new migrations automatically on the next start.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkorjavin%2Fory-compose","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkorjavin%2Fory-compose","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkorjavin%2Fory-compose/lists"}