Pocket ID on a VPS: one passkey login for all your self-hosted apps
Pocket ID is a small OIDC provider where the only way to log in is a passkey. No passwords, no TOTP codes, no LDAP to babysit. If you run several self-hosted apps and are tired of a separate login for each, a Pocket ID Docker setup on a VPS gives you one sign-in for all of them. This guide takes you from an empty compose file to a working login for a first app.
You need a VPS (not sure about size? use the VPS size calculator), Docker with Caddy as described in Docker and Caddy on a fresh server, and a domain such as id.example.com. Fresh box? Do the basic hardening first.
Pocket ID or Authentik?
Both give your apps a "Log in with..." button. They aim at different people:
- Pocket ID is deliberately simple: users, groups, OIDC clients, passkeys. The project describes itself as a provider for simple use cases, in contrast to Keycloak or Ory Hydra. Nothing else to configure.
- Authentik, Keycloak are full identity platforms: password policies, flows, LDAP and SAML, proxy outposts. Powerful, but more to run and more to learn.
For a personal server or a family, Pocket ID is usually enough. Pick a bigger system only if you actually need SAML, LDAP or custom login flows.
What you need to know about passkeys first
- Passkeys only work over HTTPS (or on
localhost). Caddy takes care of that. - A passkey is tied to your domain. If you later move Pocket ID to another domain, existing passkeys stop working. Choose
id.example.comonce and keep it. - Register at least two passkeys (for example your phone and a hardware key, or a password manager that syncs passkeys). If your only device dies and there is no second passkey, you are locked out of every app behind it.
docker-compose.yml
This follows the project's own compose file. I pin the major version tag rather than latest, so an upgrade never surprises you (see how to update containers safely).
services:
pocket-id:
image: ghcr.io/pocket-id/pocket-id:v2
restart: unless-stopped
env_file: .env
mem_limit: 256m
volumes:
- ./data:/app/data
healthcheck:
test: ["CMD", "/app/pocket-id", "healthcheck"]
interval: 1m30s
timeout: 5s
retries: 2
start_period: 10s
There is no ports: section on purpose. If Caddy runs in the same compose project (or shares a Docker network with it), only Caddy needs to reach port 1411. The 256 MB limit is my starting guess: look at docker stats after a week and adjust. If your Pocket ID is in a separate project from Caddy, put both on one shared network.
The .env file
APP_URL=https://id.example.com ENCRYPTION_KEY=PASTE_OUTPUT_OF_COMMAND_BELOW TRUST_PROXY=true
openssl rand -base64 32
APP_URLmust be the exact public address. Passkeys are bound to it.ENCRYPTION_KEYencrypts sensitive data in the database. The project also supportsENCRYPTION_KEY_FILE(a path to a file holding the key), which it recommends over putting the key in the environment. Either way, save the key in your password manager. A backup of the data without the key is not much use.TRUST_PROXY=trueis needed behind Caddy. Without it, the audit log and rate limiting see Caddy's IP instead of the real client.
chmod 600 .env
Caddyfile
id.example.com {
reverse_proxy pocket-id:1411
}
docker compose up -d docker compose exec -w /etc/caddy caddy caddy reload
Point the DNS record for id.example.com at your server first, or Caddy can't get a certificate.
Create the first admin
Open the setup page at https://id.example.com/setup, enter your name and email, and register your first passkey. Do this right away: until you do, whoever reaches that page first becomes the admin. Then, in your account settings, add a second passkey.
Connect your first app
In Pocket ID, go to OIDC Clients, add a client and give it the callback URL your app shows in its own SSO settings. Pocket ID then shows you a client ID and secret. In the app, choose "Generic OIDC" (every app names this slightly differently) and enter:
- Issuer / discovery URL:
https://id.example.com(the app appends/.well-known/openid-configurationitself, or you paste that full URL) - Client ID and client secret: from Pocket ID
- Scopes:
openid profile email(addgroupsif the app maps groups to roles)
Not every app supports OIDC. Many popular ones do: Forgejo, Immich, Miniflux, Paperless-ngx and Pangolin all have OIDC settings. Check each project's docs for the exact callback URL. Keep a local admin login in each app until you know SSO works, so a Pocket ID problem doesn't lock you out of everything.
For apps with no login of their own, such as a simple dashboard, the usual trick is a forward-auth proxy in front. That's beyond this guide. A tunnel with built-in identity login is covered in the Pangolin guide, which pairs with Pocket ID.
Harden it
- Pocket ID is the front door to everything. Keep it updated and the server patched.
- Turn off or restrict self-signup in the settings unless you really want open registration. Create users yourself and send them a one-time sign-in link.
- Use groups to limit which users may use which client.
- Don't skip SSH hardening. A passkey login doesn't help if the server itself is easy to get into.
Back it up
Everything lives in the data directory: the database, uploaded images and keys. Add /path/to/pocket-id/data to your restic backups, and store the ENCRYPTION_KEY separately in your password manager. If the database is SQLite, stop the container for a moment or use a proper SQLite backup for a consistent copy rather than copying a live file. Restore once on a test machine, so you know it works before you need it.
Common problems
- "Passkey doesn't work" after changing domains: expected. Passkeys belong to the old domain, so register new ones.
- Redirect or callback mismatch in the app: the callback URL in Pocket ID must match what the app sends, character for character, including
httpsand any trailing slash. - Wrong IP in the audit log:
TRUST_PROXYis not set totrue. - Login page works but apps say invalid issuer:
APP_URLdiffers from the URL you gave the app.
Running a handful of small services like this fits on a small plan. Compare options on the netcup VPS voucher page.