Run HAP locally with the Docker CLI (local login server)
Run the Headless Auth Platform on your own machine from the public image
tooltwist/wbsp-hap:v1.0 (Docker Hub) and use it as the OIDC/OAuth2 login server for apps you
are developing locally. This guide uses the plain docker CLI (no Docker Compose) and covers
the environment variables, the admin key ("admin account"), and the one-time tenant/app setup so a
locally-run application (e.g. on http://localhost:3000) can authenticate against it.
Scope: a self-contained local dev/test setup. It is not a production deployment — it uses console email, a single bootstrap admin key, and local datastores. For real hosting see installation-guide.md.
What you'll run
Three containers on one Docker network:
| Container | Image | Purpose |
|---|---|---|
hap-postgres | postgres:16 | PostgreSQL 16 (required; SQLite is not supported) |
hap-redis | redis:7 | Redis 7 (sessions, rate limits, revocation markers) |
hap | tooltwist/wbsp-hap:v1.0 | the auth server — serves OIDC at http://localhost:8000/t/{tenant}/… |
The HAP image applies database migrations automatically on startup (alembic upgrade head),
then serves on port 8000.
Prerequisites
- Docker installed and running (
docker version). - The image is public, so no login is needed:
docker pull tooltwist/wbsp-hap:v1.0.
Step 1 — Create a network
So the containers can reach each other by name:
docker network create hap-netStep 2 — Start PostgreSQL and Redis
docker run -d --name hap-postgres --network hap-net \
-e POSTGRES_USER=hap -e POSTGRES_PASSWORD=hap -e POSTGRES_DB=hap \
-p 54390:5432 postgres:16
docker run -d --name hap-redis --network hap-net \
-p 63390:6379 redis:7The published host ports (54390, 63390) are optional — only for inspecting the datastores from
your host. HAP reaches them inside the network on their standard ports (5432, 6379).
Wait a few seconds for Postgres to accept connections:
until docker exec hap-postgres pg_isready -U hap >/dev/null 2>&1; do sleep 1; done; echo "postgres ready"Step 3 — Generate the secrets (incl. the admin key)
HAP needs two secrets. Generate them once and keep them:
# 1. At-rest encryption key — base64-encoded 32 bytes (required by the env key provider)
openssl rand -base64 32 # e.g. on macOS/Linux
# 2. The ADMIN API KEY — this IS the platform "admin account" (see note below). Any strong string:
openssl rand -hex 24What "admin account" means here. HAP has no admin username/password. The platform administrator credential is the bootstrap admin API key you set as
HAP_ADMIN_API_KEY. You present it as theX-Admin-Keyheader to create tenants, register applications, and toggle tenant settings. Treat it like a root password. (A per-tenant tenant admin — a real user who can use the browser Admin Console — is a separate, optional concept; see the note at the end of Step 8.)
Step 4 — Write an env file
Create hap.env (one KEY=VALUE per line, no quotes), pasting the two secrets from Step 3:
# Datastores — reached by container name on the hap-net network
HAP_DATABASE_URL=postgresql+asyncpg://hap:hap@hap-postgres:5432/hap
HAP_REDIS_URL=redis://hap-redis:6379/0
# Secrets
HAP_MASTER_ENCRYPTION_KEY=<paste the base64 32-byte key>
HAP_ADMIN_API_KEY=<paste the admin key>
# Public issuer — apps use {HAP_ISSUER_BASE_URL}/t/{tenant_slug} as the OIDC issuer
HAP_ISSUER_BASE_URL=http://localhost:8000
# WebAuthn/passkeys (defaults are fine for the email-OTP login flow below)
HAP_WEBAUTHN_RP_ID=localhost
HAP_WEBAUTHN_ORIGIN=http://localhost:8000
# Runtime
HAP_ENVIRONMENT=dev
# Email: "console" prints OTP / magic-link codes to the container LOGS instead of sending
# real email — this is how you receive login codes locally. (Use "smtp" + HAP_SMTP_* for real mail.)
HAP_EMAIL_MODE=console
# Signing-key storage: "env" persists keys to /app/.keystore (mount a volume in Step 5 so they
# survive restarts). Alternatively set HAP_KEY_PROVIDER=db to keep them in Postgres (no volume needed).
HAP_KEY_PROVIDER=envKeep
hap.envout of version control — it holds your keys.
Step 5 — Start HAP
docker run -d --name hap --network hap-net -p 8000:8000 \
--env-file hap.env \
-v hap-keystore:/app/.keystore \
--restart on-failure \
tooltwist/wbsp-hap:v1.0-v hap-keystore:/app/.keystorekeeps the generated per-tenant signing keys across restarts (env key provider). Omit it if you setHAP_KEY_PROVIDER=db.--restart on-failurelets the container retry if it raced Postgres on first boot.
Watch it migrate and start:
docker logs -f hap # look for alembic running, then "Uvicorn/Gunicorn running on 0.0.0.0:8000"Step 6 — Verify it's up
curl -s localhost:8000/healthz # {"status":"ok"}
curl -s localhost:8000/readyz # {"status":"ok","checks":{"database":"ok","redis":"ok"}}If /readyz reports ok for both database and redis, the server is ready.
Step 7 — Create a tenant and enable interactive login
A fresh server has no tenants. Create one (platform-admin call — X-Admin-Key only, no
X-Tenant-Id). Use the admin key from hap.env:
ADMIN_KEY=<your HAP_ADMIN_API_KEY>
curl -s -X POST localhost:8000/v1/tenants \
-H "X-Admin-Key: $ADMIN_KEY" -H "Content-Type: application/json" \
-d '{"name":"Local","slug":"local","password_policy":{"min_length":8}}'
# → note the "id" (the tenant UUID) from the responseTurn on Hosted Login (the platform-rendered sign-in page) and just-in-time provisioning so a new email can log in without pre-creating the user. This is what lets your apps delegate login to HAP:
TENANT_ID=<id from the create response>
curl -s -X PATCH localhost:8000/v1/tenants/$TENANT_ID \
-H "X-Admin-Key: $ADMIN_KEY" -H "Content-Type: application/json" \
-d '{"hosted_login_enabled":true,"passwordless_jit_provisioning":true}'Confirm discovery resolves (this is the URL your app's OIDC client points at):
curl -s localhost:8000/t/local/.well-known/openid-configuration # JSON config = successStep 8 — Register your local application
Register the app you run on http://localhost:3000 (tenant-scoped admin call — needs both
X-Admin-Key and X-Tenant-Id). Use a confidential web client for a server-side app
(Next.js, etc.); use application_type:"spa" with confidential:false for a browser-only SPA
(PKCE, no secret):
curl -s -X POST localhost:8000/v1/applications \
-H "X-Admin-Key: $ADMIN_KEY" -H "X-Tenant-Id: $TENANT_ID" \
-H "Content-Type: application/json" \
-d '{"name":"My Local App","application_type":"web","confidential":true,
"redirect_uris":["http://localhost:3000/api/auth/callback"],
"scopes":["openid","profile","email","offline_access"]}'The response returns client_id and (for confidential clients) a client_secret — shown once,
so copy it now. redirect_uris must match your app's callback exactly; adjust the path to your
framework (e.g. Auth.js/NextAuth uses /api/auth/callback/<provider-id>).
(Optional) Promote a tenant admin for the browser console
The Admin Console at
http://localhost:8000/t/local/hosted/adminrequires a signed-in tenant admin (a user identity flaggedis_tenant_admin), which the admin API key bootstraps. It's not needed to authenticate apps via the API flow above — see hosted-login-guidelines.md and user-guide-detailed.md.
Step 9 — Point your application at this HAP
Configure your app's OIDC client with:
| Setting | Value |
|---|---|
| Issuer | http://localhost:8000/t/local |
| Discovery URL | http://localhost:8000/t/local/.well-known/openid-configuration |
| Client ID | the client_id from Step 8 |
| Client secret | the client_secret from Step 8 (confidential clients) |
| Redirect URI | http://localhost:3000/api/auth/callback (must match exactly) |
| Scopes | openid profile email offline_access |
Logging in (fully local, no real email):
- In your app, start login. It redirects the browser to
http://localhost:8000/t/local/oauth2/authorize?.... - With no session cookie, HAP renders the Hosted Login page. Enter any email address.
- Because
HAP_EMAIL_MODE=console, the one-time code is printed to the HAP logs, not emailed. Read it:docker logs --tail 20 hap. - Enter the code. HAP creates the user (JIT), sets the session cookie, and redirects the browser
back to your
redirect_uriwith an authorizationcode. - Your app exchanges the code at
http://localhost:8000/t/local/oauth2/tokenfor tokens.
That's a complete local login. See the Integration guide for the exact authorize/token request shapes.
Managing the containers
docker stop hap hap-redis hap-postgres # stop (data + keystore preserved in volumes)
docker start hap-postgres hap-redis hap # start again (order matters: datastores first)
docker logs -f hap # follow logs (OTP codes appear here)
# Full teardown (DELETES all data: users, tenants, apps, signing keys):
docker rm -f hap hap-redis hap-postgres
docker volume rm hap-keystore # only if you mounted it
docker network rm hap-netTroubleshooting
hapcontainer exits immediately / restarts —docker logs hap. Usually Postgres wasn't ready yet (it will retry with--restart on-failure) or a datastore URL is wrong.database connection not configured/redis connection not configured—HAP_DATABASE_URL/HAP_REDIS_URLare unset or unreachable. Insidehap.envthe host must be the container name (hap-postgres,hap-redis) and all three containers must share--network hap-net./readyzshowsdatabase: error— Postgres isn't healthy or credentials don't match the ones in Step 2.- Startup error about the master key —
HAP_MASTER_ENCRYPTION_KEYmust be a base64-encoded 32-byte value (Step 3). - Admin calls return
401— theX-Admin-Keyheader must equalHAP_ADMIN_API_KEY. Tenant creation/PATCH take noX-Tenant-Id; application calls require it. 404 unknown tenanton app calls —X-Tenant-Idmust be the tenant UUID from the create response, not the slug.- No login code arrives — that's expected with
HAP_EMAIL_MODE=console; the code is indocker logs hap. For real email setHAP_EMAIL_MODE=smtpand theHAP_SMTP_*vars. - Tokens become invalid after a restart — the env key provider regenerated signing keys because
/app/.keystorewasn't persisted. Mount thehap-keystorevolume (Step 5) or useHAP_KEY_PROVIDER=db.
Environment variable reference
| Variable | Required | Example / default | Purpose |
|---|---|---|---|
HAP_DATABASE_URL | yes | postgresql+asyncpg://hap:hap@hap-postgres:5432/hap | Postgres DSN |
HAP_REDIS_URL | yes | redis://hap-redis:6379/0 | Redis URL |
HAP_MASTER_ENCRYPTION_KEY | yes | base64 32 bytes | at-rest encryption (env key provider) |
HAP_ADMIN_API_KEY | yes | strong random string | the platform admin credential (X-Admin-Key) |
HAP_ISSUER_BASE_URL | recommended | http://localhost:8000 | base of the per-tenant OIDC issuer |
HAP_KEY_PROVIDER | no | env (default) / db | where tenant signing keys are stored |
HAP_WEBAUTHN_RP_ID / _ORIGIN | no | localhost / http://localhost:8000 | passkey relying party |
HAP_ENVIRONMENT | no | dev | dev | staging | prod |
HAP_EMAIL_MODE | no | console (default) / smtp | console prints OTP to logs; smtp sends mail |
PORT | no | 8000 | container listen port |
See also
- installation-guide.md — full local / Docker Compose / production install.
- getting-started.md — first-run smoke test and a login-to-token demo.
- integration-guide.md — connect an app end to end (OIDC + PKCE).
- hosted-login-guidelines.md — the platform-rendered login experience.