Files
familiar/docs/deployment.md
T
Latte f4d2dcb1d8 feat(serve): HTTP transport + hardened Docker image for secure public serving
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.
2026-07-03 19:55:20 +02:00

4.9 KiB

Deployment

familiar runs two ways from the same code and token store:

  1. Local (stdio) — the default. How Claude Desktop / Claude Code / Cowork launch it. Nothing here changes; see the top-level README.
  2. 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=http switches transports.
  • Loopback bind by default (127.0.0.1). The Docker image binds 0.0.0.0 so the reverse proxy can reach it.
  • Fail closed. Binding a non-loopback interface without FAMILIAR_HTTP_AUTH_TOKEN refuses to start (override only with the explicit FAMILIAR_ALLOW_INSECURE_BIND=true escape 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 proxy network; not published to the host.
  • Runs non-root, read_only rootfs + 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_TOKEN and restarting; update every client header.
  • Restrict inbound at the proxy/firewall to the clients you expect.
  • Watch docker compose logs for 401 auth failures and the /data/audit.log write trail.
  • Re-login (host) whenever you change requested scopes, then refresh the /data token store.