Add an opt-in streamable-HTTP transport alongside the default stdio, so familiar can be reached from a home lab over HTTPS behind a TLS reverse proxy. - config.http_settings(): env-driven transport/host/port/path/token settings. - serve.run(): dispatch stdio vs http; fail closed on a non-loopback bind without a bearer token (FAMILIAR_ALLOW_INSECURE_BIND escape hatch). - server.build_http_auth(): FastMCP StaticTokenVerifier bearer gate; add an unauthenticated /healthz route for container/proxy probes. - `familiar serve` CLI alias; clean exit on InsecureBind. - docker/Dockerfile (multi-stage, non-root) + hardened docker-compose.yml (read-only rootfs, cap_drop ALL, no-new-privileges, Traefik TLS labels), .dockerignore, .env.example. Token store mounts at /data; writes off by default. - .gitea/workflows/docker.yml: build + smoke-test, opt-in image publish. - docs/deployment.md + README/CLAUDE.md notes; tests for settings, fail-closed validation, and the bearer verifier.
4.9 KiB
Deployment
familiar runs two ways from the same code and token store:
- Local (stdio) — the default. How Claude Desktop / Claude Code / Cowork launch it. Nothing here changes; see the top-level README.
- Public (HTTP) — opt-in. Serve streamable HTTP from Docker in your home lab, behind a TLS reverse proxy, gated by a bearer token.
This page covers (2).
Secure defaults
- stdio unless told otherwise.
FAMILIAR_TRANSPORT=httpswitches transports. - Loopback bind by default (
127.0.0.1). The Docker image binds0.0.0.0so the reverse proxy can reach it. - Fail closed. Binding a non-loopback interface without
FAMILIAR_HTTP_AUTH_TOKENrefuses to start (override only with the explicitFAMILIAR_ALLOW_INSECURE_BIND=trueescape hatch). - Writes off on the public image (
FAMILIAR_ENABLE_WRITES=false) — smaller blast radius if the token ever leaks. Local runs keep writes on. - Token never in the URL. Clients authenticate with an
Authorization: Bearer <token>header only.
Environment variables
| Variable | Default | Meaning |
|---|---|---|
FAMILIAR_TRANSPORT |
stdio |
stdio or http. |
FAMILIAR_HTTP_HOST |
127.0.0.1 |
Bind address (image sets 0.0.0.0). |
FAMILIAR_HTTP_PORT |
8000 |
Bind port. |
FAMILIAR_HTTP_PATH |
/mcp |
MCP endpoint path. |
FAMILIAR_HTTP_AUTH_TOKEN |
(unset) | Bearer token. Required for any non-loopback bind. |
FAMILIAR_ALLOW_INSECURE_BIND |
false |
Allow a public bind without a token (don't). |
FAMILIAR_DATA_DIR |
per-user | Where token.enc + key + audit.log + write toggle live. Image sets /data. |
FAMILIAR_ENABLE_WRITES |
on | false disables all guarded writes. |
/healthz is an unauthenticated liveness probe (used by the Docker healthcheck);
every MCP call under FAMILIAR_HTTP_PATH requires the bearer token.
One-time login (why it happens on the host)
EVE SSO login is a browser PKCE flow with a localhost callback — it can't run
headless in a container. So authenticate once on a machine with a browser,
then hand the resulting token store to the container. The container only ever
refreshes tokens; it never opens a browser.
uv run familiar login # opens the browser, writes token.enc + key
uv run familiar status # confirm the character(s)
Then make that data dir available to the container. Point
FAMILIAR_DATA_DIR_HOST at it, or copy token.enc + key into ./data:
mkdir -p ./data
# Linux/macOS host default:
cp ~/.local/share/familiar/{token.enc,key} ./data/
# Windows host default:
# copy %LOCALAPPDATA%\familiar\token.enc .\data\ ; copy %LOCALAPPDATA%\familiar\key .\data\
The /data volume is mounted read-write so token refresh (which rotates the
refresh token) and the audit log can persist.
Run with Docker Compose
cp .env.example .env
# edit .env: set FAMILIAR_HTTP_AUTH_TOKEN (strong!), FAMILIAR_DOMAIN, cert resolver
python -c "import secrets; print(secrets.token_urlsafe(32))" # generate a token
docker network create proxy # once, if your reverse proxy doesn't own it yet
docker compose up -d --build
docker compose logs -f familiar
The compose service:
- Joins the external
proxynetwork; not published to the host. - Runs non-root,
read_onlyrootfs + tmpfs/tmp,no-new-privileges,cap_drop: ALL. - Ships Traefik router labels for TLS (Let's Encrypt) — enable/disable to taste.
- Healthchecks
GET /healthz.
TLS: Traefik (labels, included)
The compose file already carries Traefik labels. Set in .env:
FAMILIAR_DOMAIN=familiar.yourdomain.tld
TRAEFIK_CERTRESOLVER=letsencrypt
Traefik terminates HTTPS and forwards to the container's port 8000 on the
proxy network. Your public URL is then https://familiar.yourdomain.tld/mcp.
TLS: Caddy (alternative)
If your home lab runs Caddy instead, drop the Traefik labels and add to your
Caddyfile (Caddy must share the proxy network / reach the container):
familiar.yourdomain.tld {
reverse_proxy familiar:8000
}
Caddy fetches and renews the certificate automatically.
Connect a client
Point an MCP client at the HTTPS endpoint with the bearer token:
claude mcp add --transport http familiar https://familiar.yourdomain.tld/mcp \
--header "Authorization: Bearer <your-token>"
Claude Desktop / other clients: configure an HTTP (streamable) MCP server with
URL https://familiar.yourdomain.tld/mcp and the same Authorization header.
Operational notes
- Rotate the token by changing
FAMILIAR_HTTP_AUTH_TOKENand restarting; update every client header. - Restrict inbound at the proxy/firewall to the clients you expect.
- Watch
docker compose logsfor401auth failures and the/data/audit.logwrite trail. - Re-login (host) whenever you change requested scopes, then refresh the
/datatoken store.