Access & accounts

Letting other people in, safely.

Three related jobs: reaching your server from outside the house, giving everyone one login instead of nine, and deciding who can actually do what. They have to be done in that order, because single sign-on needs a real HTTPS address before it will work at all.

Start here

Do these in order

Each step depends on the one before it. Skipping ahead is the main reason people get stuck — particularly with passkeys, which simply refuse to work without HTTPS.

Get a domainHTTPS workingInstall Pocket IDConnect appsAdd people
Passkeys require a secure context. Browsers only allow them on https:// with a real domain name, or on localhost. You cannot run Pocket ID at http://192.168.1.50:1411 and register a passkey — the browser blocks it, and no setting changes that. Sort out HTTPS first.
You may not need any of this. If you're the only user and you're happy reaching things through Tailscale, you can stop after part one. Single sign-on earns its keep when several people need accounts across several apps — not before.
Part one

Reaching your server

Your server sits on your home network. The internet can't see it, which is a feature. There are three ways to change that, and they differ enormously in how much they expose.

Choosing an approach

Start here

Tailscale

A private network between your own devices. Nothing is published to the internet; your phone simply behaves as though it's at home.

  • Set up in minutes
  • Works around CGNAT
  • No ports opened
  • Only for people you invite
For sharing

Cloudflare Tunnel

A public address with no open ports. An outbound connection from your server does the work, so it also beats CGNAT.

  • Real public URL
  • Router untouched
  • Cloudflare terminates TLS
  • Media streaming is unsettled

Setup steps →

Most control

Reverse proxy

Your own server answering on ports 80 and 443, forwarded through your router.

  • Full control, no middleman
  • Needs a static-ish IP or DDNS
  • Genuinely public — hardening matters
  • Required for Pocket ID
Which should you pick?

If it's just you and your own devices, use Tailscale and skip the rest of this page. If you want family members to watch Jellyfin on a TV without installing anything, you need a public address — a reverse proxy or Cloudflare Tunnel. Pocket ID needs a public HTTPS address too, so single sign-on implies one of the latter two.

Tailscale

The least you can do while still getting remote access, and for many people it's genuinely enough.

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

Install the app on your phone and laptop, sign into the same account, and your server appears at a stable address like http://server:8096. No ports opened, nothing public.

Sharing with family without giving them your account. Tailscale can share an individual machine with another Tailscale user, and Funnel can publish a single service publicly if you want one exception to the rule. Both are narrower than opening a port.

Reverse proxy with Caddy

A reverse proxy sits in front of everything, terminates HTTPS, and routes by hostname. Caddy is the friendliest option because it obtains and renews certificates without being asked.

  1. Point a domain at your home

    Buy a domain, then create records pointing at your public address. If your address changes — most home connections — use a dynamic DNS updater, or Cloudflare's API with a small updater container.

    # One record per service, or a wildcard
    jellyfin.example.com  →  your.public.ip
    id.example.com        →  your.public.ip
  2. Forward two ports

    In your router, forward 80 and 443 to your server. Those two only — never the application ports themselves.

    Port 80 is needed for certificate issuance, not for serving your site. Caddy redirects it to HTTPS automatically once certificates exist.
  3. Add Caddy to your stack

      caddy:
        image: caddy:latest
        container_name: caddy
        ports:
          - "80:80"
          - "443:443"
        volumes:
          - ./Caddyfile:/etc/caddy/Caddyfile:ro
          - ${CONFIG_ROOT}/caddy/data:/data
          - ${CONFIG_ROOT}/caddy/config:/config
        restart: unless-stopped

    The /data volume holds your certificates. Losing it means re-issuing them, and issuers apply rate limits — so keep it.

  4. Write the Caddyfile

    One block per service. Caddy handles certificates for each name automatically.

    id.example.com {
        reverse_proxy pocket-id:1411
    }
    
    jellyfin.example.com {
        reverse_proxy jellyfin:8096
    }
    
    photos.example.com {
        reverse_proxy immich-server:2283
    }

    Services are addressed by container name, because Caddy is on the same Docker network — not localhost, which inside Caddy's container means Caddy.

    Testing without burning your rate limit. Certificate authorities limit failed attempts. While experimenting, add acme_ca https://acme-staging-v02.api.letsencrypt.org/directory at the top of the Caddyfile. Certificates won't be trusted by browsers, but you can confirm routing works, then remove the line.
  5. Bring it up and check

    docker compose up -d caddy
    docker compose logs -f caddy

    Watch for certificate issuance. Errors here are almost always DNS not resolving yet, or port 80 not actually reaching the server.

Reloading after a change

Adding a service means adding a block and reloading. A reload swaps the configuration with no dropped connections — it is not a restart:

# check the syntax first
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile

# apply it with no downtime
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

The Caddyfile is mounted live, so editing it on the host or through Arcane is enough — only the reload is needed.

The generator writes this file for you, containing only the services that are safe to publish. See the generator for where it goes and how to test certificate issuance without hitting rate limits.

What to never expose

Publishing a hostname makes it reachable by everyone, including automated scanners that will find it within hours. Some things in this stack must not be among them.

ServicePublic?Why
JellyfinReasonableDesigned for it, has real user accounts
JellyseerrReasonableBuilt for household requests
ImmichReasonableNeeds it for phone backup away from home
Pocket IDRequiredCan't function otherwise
ArcaneNeverHolds the Docker socket — root on your machine
qBittorrentNeverCan write files anywhere it can reach
Sonarr / Radarr / LidarrNeverWeak auth, can execute scripts on import
JackettNeverNo meaningful authentication
Uptime KumaStatus page onlyPublish the status page, not the admin interface

Reach the "never" items through Tailscale instead. You can run both: a public proxy for the things family use, and a private network for everything administrative.

Part two

Pocket ID

Pocket ID is a small, OpenID Connect certified identity provider built entirely around passkeys. One login for every app that supports it, no passwords stored anywhere, and a single Go binary with a SQLite database rather than the sprawl of Keycloak.

What a passkey actually is

A key pair held by your phone, laptop or a physical security key, unlocked by your fingerprint, face or device PIN. The private half never leaves the device and is never sent anywhere. There's no password to reuse, leak or guess — which removes most of the ways accounts get broken into.

Passkeys only work over HTTPS on a real domain. This is a browser rule, not a Pocket ID setting. Finish part one first — you need https://id.example.com resolving and trusted before anything below will work.

Installing it

  1. Generate an encryption key

    openssl rand -base64 32

    Put it in your .env. Pocket ID uses it to encrypt sensitive values at rest.

    POCKETID_ENCRYPTION_KEY=your-generated-key
    POCKETID_URL=https://id.example.com
  2. Add the service

      pocket-id:
        image: ghcr.io/pocket-id/pocket-id:v2
        container_name: pocket-id
        ports:
          - "1411:1411"
        volumes:
          - ${CONFIG_ROOT}/pocket-id:/app/data
        environment:
          - APP_URL=${POCKETID_URL}
          - ENCRYPTION_KEY=${POCKETID_ENCRYPTION_KEY}
          - TRUST_PROXY=true
          - PUID=${PUID}
          - PGID=${PGID}
        restart: unless-stopped

    APP_URL must exactly match the address you'll visit, including https://. Passkeys are bound to that origin, so a mismatch means they silently stop working.

    TRUST_PROXY=true tells it to honour the forwarded headers from Caddy. Without it, it sees every request as coming from the proxy.

    The default database is SQLite, which is fine for a household. Pocket ID also supports Postgres via DB_PROVIDER if you'd rather consolidate databases.
  3. Add it to the Caddyfile and start

    id.example.com {
        reverse_proxy pocket-id:1411
    }
    docker compose up -d pocket-id caddy

    Visit https://id.example.com. You should get a valid certificate and a login page. If the certificate isn't trusted, fix that before continuing — passkeys will not register.

Creating your first passkey

There's no admin password in an environment variable, because there are no passwords at all. On first run a setup wizard creates the initial admin account, and you register a passkey immediately afterwards.

If you need to get back in later without a working passkey, Pocket ID's CLI issues a one-time access link:

docker compose exec pocket-id /app/pocket-id one-time-access-token admin

That prints a URL valid once, for a short window. Open it and register a new passkey.

Register at least two passkeys before you rely on this. One on your phone, one on a laptop or a hardware key kept somewhere safe. A single passkey on a single device means a lost phone locks you out of every connected app at once. This is the most important paragraph on this page.

Adding an application

Every app you connect needs a client entry. The process is the same each time.

  1. Create the client

    In Pocket ID, go to Administration → OIDC Clients → Add OIDC Client. Give it the app's name and set the callback URL, which each application specifies in its own documentation.

  2. Copy the credentials

    You'll get a Client ID and a Client Secret. The secret is shown once. Store it in your .env rather than pasting it into a Compose file.

  3. Point the app at Pocket ID

    Most apps only need the issuer URL and will discover the rest themselves:

    https://id.example.com/.well-known/openid-configuration
Part three

Connecting your apps

This is where expectations need managing. Single sign-on across a media stack is not the solved problem it appears to be — most of these applications have no OIDC support at all, and the most important one has an awkward caveat.

What actually supports it

AppOIDCReality
ImmichNativeWorks properly, including the mobile app
ArcaneNativeSupported directly
JellyfinPluginCommunity plugin, web browser only — see below
JellyseerrVariesSigns in through Jellyfin; check your version's options
Sonarr / Radarr / LidarrNoneBasic forms login only
BazarrNoneBasic login only
qBittorrentNoneIts own single account
Uptime KumaNoneIts own accounts
ArchiveBoxNoneDjango accounts, LDAP possible
Tube ArchivistNoneIts own accounts
So what's it actually worth?

Realistically, Pocket ID gives you one login for Immich, Arcane and Jellyfin in a browser. That's a genuine improvement if those are the apps other people touch. It does not give you one login for the whole stack, and anyone claiming otherwise hasn't tried it on a television.

Immich

The best-supported of the three, and the one where SSO is most worth having, since it's the app family members use daily on their phones.

In Pocket ID, create a client with these callback URLs:

https://photos.example.com/auth/login
https://photos.example.com/user-settings
app.immich:///oauth-callback   # for the mobile app

Then in Immich, under Administration → Settings → OAuth:

Issuer URLhttps://id.example.com/.well-known/openid-configuration
Client IDfrom Pocket ID
Client Secretfrom Pocket ID
Scopeopenid email profile
Auto registeron, to create accounts on first login
Don't disable password login until you've tested it. Immich can hide the password form entirely. Do that only once OIDC login has worked in a private browser window — otherwise a misconfiguration locks you out of your own photo library.

Jellyfin — read this before starting

Jellyfin has no native OpenID Connect support. There's a community SSO plugin that works well, but it carries a limitation that matters enormously for a media server.

The plugin authenticates through the web interface or Quick Connect only. Native clients — Swiftfin, Jellyfin for Android and Android TV, Roku, Kodi — cannot use the SSO redirect flow directly. In practice, if your family watches on a TV app, every one of them still needs a normal Jellyfin account regardless of what you set up here.

Native OIDC has been proposed upstream and is being worked on, but it isn't in a released version at the time of writing, and the discussion points at a future major release. Until then, this is the situation.

Given that, decide honestly whether it's worth it:

Worth doing ifPeople mostly use the web interface, you want central account control, or you're already running Pocket ID for Immich anyway.
Skip it ifEveryone watches on TV apps. You'll maintain SSO that nobody's device can use, alongside the passwords they'll actually use.

If you're proceeding

The original plugin was archived in May 2026. Use the maintained fork, which keeps the same plugin ID (so it upgrades in place) and adds a proper admin UI. Add its repository under Administration → Plugins → Repositories, install SSO Authentication from the catalogue, and restart Jellyfin.

https://raw.githubusercontent.com/K0lin/jellyfin-plugin-sso/manifest-release/manifest.json
Other forks exist, and at least one is explicitly pre-alpha. A plugin repository can install code on your server, so check what you are adding.

In Pocket ID, create a client whose callback URL follows the plugin's pattern — the provider name at the end is whatever you choose to call it, and must match on both sides:

https://jellyfin.example.com/sso/OID/redirect/pocketid

Configure the plugin with your issuer URL, client ID and secret, enable it, tick folder access, and restart Jellyfin — configuration changes don't take effect until you do.

The login button isn't added automatically. Add a custom link in Jellyfin's config/web/config.json:

{
  "customLinks": [
    { "name": "Sign in with SSO",
      "url": "/sso/OID/start/pocketid",
      "icon": "lock" }
  ]
}
Test in a private window. Your existing session will mask problems. And keep one local admin account with a password — if the plugin breaks after an update, that account is how you get back in.

Everything else

For apps with no OIDC support, you have three honest options.

Recommended

Keep them private

Don't publish them at all. Reach Sonarr, Radarr, qBittorrent and Arcane through Tailscale. Their own login is then a second layer, not the only one.

More work

Forward auth

A proxy component checks authentication before passing the request on, putting a login in front of apps that don't support one.

Simplest

Just use their logins

Set a strong unique password on each. Unglamorous, but these are admin tools you touch rarely, and a password manager makes it a non-issue.

Pocket ID alone cannot protect apps that don't speak OIDC. It's an identity provider, not a gateway — it can't sit in front of Sonarr and demand a login. Forward authentication needs an additional component such as oauth2-proxy, or a proxy with an auth module. If that sounds like more moving parts than you want, the first option is genuinely the better engineering choice for a home setup.
Part four

User management

Accounts live in several places at once, and knowing which system owns what saves a lot of confusion later.

SystemOwns
Pocket IDWho a person is, their passkeys, and group membership
JellyfinWhat they can watch, and playback permissions
JellyseerrWhat they may request, and how much
ImmichTheir photo library and sharing

Pocket ID authenticates. Each app authorises. Deleting someone from Pocket ID stops them logging in through it, but doesn't remove their Jellyfin account or their data — those need handling separately.

Groups

Create groups in Pocket ID and assign people to them. Applications that read group claims can map them to their own permissions.

media-usersEveryone who watches things. Standard access, no administrative rights.
media-adminsYou, and anyone you genuinely trust with settings.

Two groups is usually plenty for a household. Elaborate role hierarchies are for organisations, and they mostly create work for you.

Setting up the household

  1. Create the person in Pocket ID

    Add them under Users, then send them a one-time link so they can register their own passkey on their own device. You never handle a credential for them.

  2. Give them a Jellyfin account

    Because TV apps can't use SSO, create a normal Jellyfin user too. Set library access, and turn off downloading and management unless they need it.

    Untick "Allow media playback that requires transcoding" for casual users if your server is CPU-limited. It's the difference between one person's device quietly overloading your machine and it politely refusing.
  3. Add them to Jellyseerr

    Import users from Jellyfin, then set request quotas — something like five movies a week prevents enthusiastic new users filling your disk in an afternoon. Leave auto-approve off until you know how they use it.

  4. Immich, if they want it

    With auto-registration on, they just sign in and get their own library. Photos are private per user unless explicitly shared, so this needs no configuration from you.

  5. Write down what you did

    Keep a short note of which accounts exist where. In a year, when someone leaves or an app misbehaves, you'll want it — and it takes two minutes now.

Removing someone

Because accounts are spread across systems, offboarding is a checklist rather than one button.

  • Delete or disable them in Pocket ID — stops SSO logins immediately
  • Delete their Jellyfin user — their TV apps keep working until you do
  • Remove them from Jellyseerr, so pending requests don't linger
  • Decide what happens to their Immich library before deleting the account, since that removes their photos
  • Revoke active sessions in Jellyfin, or a logged-in device continues to play
Deleting the Pocket ID account is not enough on its own. Anyone with a Jellyfin password and a TV app never touches Pocket ID, and will carry on watching indefinitely. This surprises people.
Before you call it done

Security checklist

  • Only ports 80 and 443 forwarded — never application ports
  • Arcane, qBittorrent, Sonarr, Radarr and Jackett are not publicly reachable
  • At least two passkeys registered, on separate devices
  • One local admin account kept on Jellyfin as a fallback
  • Every remaining app has a unique password, stored in a password manager
  • Certificates renewing automatically — check once after 60 days
  • Caddy's /data volume included in your backups
  • .env is mode 600 and excluded from git
  • You've tested logging in from outside your network, in a private window
  • You know how to get back in if Pocket ID is unavailable
Publishing anything to the internet changes your risk. Automated scanners find new hostnames within hours of a certificate being issued. That's normal and not cause for alarm, but it does mean weak authentication gets discovered rather than overlooked. Keep things updated, keep the administrative tools private, and expose only what genuinely needs to be reachable.

Troubleshooting   Setup guide   Documentation