Deploy Pocket ID | (Just Updated) OIDC Provider with Passkeys, Admin and API Key Preset, Data Survives Redeploys

Pocket ID OIDC provider: passkey login, API key and admin preset

Deploy Pocket ID | (Just Updated) OIDC Provider with Passkeys, Admin and API Key Preset, Data Survives Redeploys

Just deployed

/app/data

Deploy and Host Pocket ID on Railway

Built for an LLM agent to deploy and use. There is no setup page to open and nothing to type into the deploy form: the admin account is created on first boot, the API key and encryption key are generated, and the contract below lists every address, variable name and one script that registers an OIDC client and exchanges it for a token.

Pocket ID is an open source OpenID Connect (OIDC) identity provider that signs users in with passkeys, so there are no passwords to store. Your apps use it as their single sign-on server: Grafana, Portainer, Immich, Gitea and anything else that speaks OIDC. It is a single Go binary with a SQLite database and uses little memory.

About Hosting Pocket ID

This template runs one service: the official ghcr.io/pocket-id/pocket-id v2.19.2 image, pinned by digest, with a volume on /app/data. The volume holds the SQLite database, the signing keys and uploaded logos, so users, passkeys and OIDC clients survive redeploys. ENCRYPTION_KEY is generated once and kept as a variable, so the data stays readable after a redeploy.

Pocket ID's own first-run page makes whoever opens it first the administrator. On a public URL that is a race you can lose, so the start command closes it: it boots the server on the loopback address only, creates the admin user, stops it and then starts the real server on all interfaces. On a redeploy the admin already exists and the step does nothing. The address in APP_URL is your Railway domain, which is the domain every passkey is bound to, so add a custom domain only before you register passkeys.

Agent contract

  • Public URL: POCKET_ID_PUBLIC_URL (https:// plus the service's RAILWAY_PUBLIC_DOMAIN). The OIDC issuer is this URL, discovery is at /.well-known/openid-configuration and the admin UI is at /.
  • Private URL: http://pocket-id.railway.internal:8080 (POCKET_ID_PRIVATE_URL). Use the public URL as the issuer in your app, since that is the issuer Pocket ID signs.
  • Credentials: STATIC_API_KEY (full admin API access) and ENCRYPTION_KEY (do not share, do not change after first boot).
  • Auth: send the header X-API-KEY: $STATIC_API_KEY on every /api/ call. The admin user is admin; it has no passkey until you mint a one-time login link (last lines of the script) and open it.
  • Health: GET /healthz returns 204 without credentials.
  • API: GET /api/users, POST /api/users/{id}/one-time-access-token, GET and POST /api/oidc/clients, POST /api/oidc/clients/{id}/secrets, DELETE /api/oidc/clients/{id}, POST /api/oidc/token.
# run with POCKET_ID_PUBLIC_URL and STATIC_API_KEY set
U="$POCKET_ID_PUBLIC_URL"; K="$STATIC_API_KEY"
curl -s -o /dev/null -w "healthz: %{http_code}\n" "$U/healthz"
curl -s "$U/.well-known/openid-configuration" | sed -n 's/.*"issuer":"\([^"]*\)".*/issuer: \1/p'
curl -s -o /dev/null -w "no key: %{http_code}\n" "$U/api/users"
curl -s -H "X-API-KEY: $K" "$U/api/users" | grep -o '"username":"admin"' | head -1
ID=$(curl -s -H "X-API-KEY: $K" -H "Content-Type: application/json" -d '{"name":"agent-test","callbackURLs":["https://app.example.com/callback"],"isPublic":false,"pkceEnabled":true}' "$U/api/oidc/clients" | sed -n 's/^{"id":"\([^"]*\)".*/\1/p')
SECRET=$(curl -s -X POST -H "X-API-KEY: $K" "$U/api/oidc/clients/$ID/secrets" | sed -n 's/.*"secret":"\([^"]*\)".*/\1/p')
curl -s -o /dev/null -w "token: %{http_code}\n" -d grant_type=client_credentials -d client_id="$ID" -d client_secret="$SECRET" "$U/api/oidc/token"
curl -s -o /dev/null -w "delete client: %{http_code}\n" -X DELETE -H "X-API-KEY: $K" "$U/api/oidc/clients/$ID"
AID=$(curl -s -H "X-API-KEY: $K" "$U/api/users" | sed -n 's/.*"id":"\([^"]*\)","username":"admin".*/\1/p')
TOKEN=$(curl -s -X POST -H "X-API-KEY: $K" -H "Content-Type: application/json" -d '{}' "$U/api/users/$AID/one-time-access-token" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
echo "admin login link (open once, then add a passkey): $U/lt/$TOKEN"

A fresh deploy prints healthz: 204, the issuer (your public URL), no key: 401, "username":"admin", token: 200, delete client: 204 and a login link. In your own app, use the client id and the secret from the /secrets call as the OIDC client id and secret, the public URL as the issuer, and your app's callback URL as callbackURLs.

Why Deploy Pocket ID on Railway?

Railway is a singular platform to deploy your infrastructure stack. Railway will host your infrastructure so you don't have to deal with configuration, while allowing you to vertically and horizontally scale it.

By deploying Pocket ID on Railway, you are one step closer to supporting a complete full-stack application with minimal burden. Host your servers, databases, AI agents, and more on Railway.

  • Agent-ready - generated keys, a ready admin account, a contract block and one working script; nothing to type into the deploy form.
  • No first-visitor takeover - the admin is created before the server is reachable, so nobody else can claim it from the setup page.
  • State survives redeploys - the database and signing keys live on a volume mounted at the path Pocket ID v2 actually writes to, and the image is pinned by digest.
  • A healthcheck that works - /healthz is checked on every deploy, so a bad deploy is caught instead of served.
  • One light service - no external database or cache to run alongside.

Common Use Cases

  • Single sign-on for a self-hosted stack - one login with a passkey for Grafana, Gitea, Portainer, Immich and other OIDC apps.
  • Auth for an agent-built app - an AI agent registers an OIDC client through the API, wires the issuer and secret into the app, and deletes it when done.
  • Replace Keycloak or Authentik for a small team - passkey-only sign-in with users, groups and per-client access, without running a heavy identity stack.

Dependencies for Pocket ID Hosting

  • One Railway volume on /app/data, created by the template.

Deployment Dependencies

Implementation Details

VariablePurpose
ENCRYPTION_KEYEncrypts the signing keys in the database. Generated per deploy; keep it, since changing it makes stored keys unreadable.
STATIC_API_KEYAdmin-level API key. Generated per deploy.
APP_URLHTTPS address of the instance. It is the OIDC issuer and the domain passkeys are bound to.
POCKET_ID_PUBLIC_URLSame address, named for the contract above.
POCKET_ID_PRIVATE_URLAddress on the private network.

The start command sets TRUST_PROXY, turns off usage analytics, creates the admin user through the setup endpoint while the server listens on loopback only, then runs the image's own entrypoint, which fixes ownership of the volume and drops to a non-root user.


Template Content

More templates in this category

View Template
Keycloak
[2026-11-04] Keycloak with keywind theme + Apple & Discord providers

beuz
755
View Template
lua-protector
Test deployed my project first

trianaq765-cmd's Project
37
View Template
bknd
Feature-rich yet lightweight backend

10