Run doorman locally, put a site behind it, and unlock it with a PIN — no TLS, DNS or reverse proxy needed.
Everything below runs on one docker host. No certificate, no DNS, no Traefik — those belong to self-hosting, not to trying it.
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.
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
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.
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.
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.
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.