Quickstart

beta

Run doorman locally, put a site behind it, and unlock it with a PIN — no TLS, DNS or reverse proxy needed.

Quickstart

Everything below runs on one docker host. No certificate, no DNS, no Traefik — those belong to self-hosting, not to trying it.

1. Run doorman

docker network create doorman-demo

docker run -d --name doorman \
  --network doorman-demo \
  -p 8080:8080 \
  -v doorman-data:/data \
  -e DOORMAN_ADMIN_TOKEN=dev-token \
  registry.agentics.dk/agentics/pks-agent-doorman:latest

/data holds sites, PINs, links, the audit log and the Data Protection key ring. Keep it: a fresh key ring invalidates every cookie and every link you have already sent out.

2. Give it something to protect

Any container on the same network will do. The important part is the network alias — that, not the container name, is how doorman addresses it.

docker run -d --name demo-site \
  --network doorman-demo --network-alias griller \
  nginx:alpine

3. Create the site

curl -X PUT http://127.0.0.1:8080/_doorman/api/v1/sites/griller \
  -H 'Authorization: Bearer dev-token' \
  -H 'Content-Type: application/json' \
  -d '{
        "displayName": "Griller.dk",
        "hosts": ["griller.localtest.me"],
        "upstream": { "host": "griller", "port": 80 },
        "access": { "pinEnabled": true },
        "injection": { "banner": true }
      }'

Changes apply immediately — the store reloads and the proxy re-binds its routes without a restart.

4. Watch it refuse you

Doorman picks the site from the Host header, so every request needs one:

curl -si -H 'Host: griller.localtest.me' \
  -H 'Accept: text/html' -H 'Sec-Fetch-Mode: navigate' \
  http://127.0.0.1:8080/ | head -1
# HTTP/1.1 302 Found  →  /_doorman/gate

A hostname doorman does not know gets a neutral 404, never a hint that something is there.

5. Mint a PIN and go in

curl -X POST http://127.0.0.1:8080/_doorman/api/v1/sites/griller/pins \
  -H 'Authorization: Bearer dev-token' \
  -H 'Content-Type: application/json' \
  -d '{"label": "Anna hos kunden"}'
# → { "code": "GRIL-4821", … }

The code comes from an alphabet with no O/0, I/1 or S/5 — it gets read aloud on the phone and typed by people who did not choose it.

Post it to the gate, keep the cookie, and browse:

curl -si -H 'Host: griller.localtest.me' \
  -d 'code=GRIL-4821' http://127.0.0.1:8080/_doorman/gate | grep -i set-cookie

curl -s -H 'Host: griller.localtest.me' -H 'Cookie: <the cookie>' \
  http://127.0.0.1:8080/ | grep -i robots
# <meta name="robots" content="noindex, nofollow, noarchive">

The nginx welcome page comes back with the noindex meta and the preview banner injected. Ask for a non-HTML path and it passes through byte-for-byte — doorman only ever rewrites text/html.

6. See who was in

curl -s http://127.0.0.1:8080/_doorman/api/v1/sites/griller/audit \
  -H 'Authorization: Bearer dev-token'

One line per unlock: method, subject, address, time. That log is the thing a shared password can never give you.

Where to next

  • Self-hosting — the wildcard host, the certificate, and adding customers with no infrastructure work.
  • Admin API — magic links, share links, expiry, staff SSO and everything else the gate can be told to do.