SELFHOST.COMPUTER

Pocket ID on a VPS: one passkey login for all your self-hosted apps

#pocket-id#docker#security

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.com once 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_URL must be the exact public address. Passkeys are bound to it.
  • ENCRYPTION_KEY encrypts sensitive data in the database. The project also supports ENCRYPTION_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=true is 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-configuration itself, or you paste that full URL)
  • Client ID and client secret: from Pocket ID
  • Scopes: openid profile email (add groups if 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 https and any trailing slash.
  • Wrong IP in the audit log: TRUST_PROXY is not set to true.
  • Login page works but apps say invalid issuer: APP_URL differs 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.

< all posts · netcup voucher codes

Keep reading