Files
familiar/doc/deployment.md
T
Latte ef1567b071
CI / lint (pull_request) Successful in 53s
docker / lint (pull_request) Successful in 53s
CI / test (pull_request) Successful in 1m3s
CI / publish (pull_request) Has been skipped
docker / test (pull_request) Successful in 57s
docker / docker (pull_request) Failing after 1m7s
chore(deploy): drop Traefik labels, document Pangolin as the proxy
The compose service already joins the external `proxy` network and
exposes :8080; the reverse proxy owns TLS and routing. Remove the
Traefik router labels (and the now-unused FAMILIAR_DOMAIN /
TRAEFIK_CERTRESOLVER env vars) and document Pangolin — which discovers
the service on the shared network — as the proxy instead. nginx/Caddy
guidance is unchanged.
2026-07-05 13:38:51 +02:00

7.5 KiB

Deployment — familiar as a remote MCP server

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 getting-started.
  2. Remote (HTTP) — opt-in via familiar serve. Streamable HTTP behind a TLS reverse proxy, gated by a bearer token (or external OAuth). This page covers (2).

Once it's running, see doc/clients/ for exact per-client setup.

Secure defaults

  • stdio unless told otherwise. familiar serve (not the bare familiar) starts HTTP.
  • 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. familiar refuses to start without authentication. In bearer mode that means no FAMILIAR_HTTP_TOKEN → it won't boot; a non-loopback bind without a token is called out explicitly. The only override is the explicit FAMILIAR_ALLOW_INSECURE_BIND=true escape hatch.
  • Writes off over HTTP. A network-reachable, LLM-driven endpoint keeps writes disabled unless you set FAMILIAR_ENABLE_WRITES=true. Local stdio keeps writes on.
  • Token never in the URL. Clients authenticate with an Authorization: Bearer <token> header only.

Authentication modes

FAMILIAR_AUTH_MODE picks how the HTTP transport authenticates:

  • bearer (default) — one static token in FAMILIAR_HTTP_TOKEN. Simple, and every client below except Claude web supports it. Recommended.
  • oauth — familiar runs as an OAuth 2.1 resource server: it validates incoming JWT access tokens against an external issuer's JWKS and serves /.well-known/oauth-protected-resource/<path> so OAuth-only clients can discover the authorization server. familiar is not an authorization server and does no Dynamic Client Registration — you must run an external IdP that does (Authentik, Keycloak, …). Only needed for the native Claude web connector. See doc/clients/claude-web.md.

Environment variables

Variable Default Meaning
FAMILIAR_HTTP_HOST 127.0.0.1 Bind address (image sets 0.0.0.0).
FAMILIAR_HTTP_PORT 8080 Bind port.
FAMILIAR_HTTP_PATH /mcp MCP endpoint path.
FAMILIAR_AUTH_MODE bearer bearer or oauth.
FAMILIAR_HTTP_TOKEN (unset) Bearer token. Required in bearer mode.
FAMILIAR_ALLOW_INSECURE_BIND false Allow a bind without a token (don't).
FAMILIAR_OAUTH_ISSUER (unset) Authorization-server URL (oauth mode).
FAMILIAR_OAUTH_RESOURCE (unset) This server's public URL, e.g. https://familiar.example.com (oauth mode).
FAMILIAR_OAUTH_AUDIENCE = resource Expected token audience (oauth mode).
FAMILIAR_OAUTH_JWKS_URI (discovered) JWKS URL; if unset, discovered from the issuer's OIDC metadata.
FAMILIAR_DATA_DIR per-user Where token.enc + key + audit.log + write toggle live. Image sets /data.
FAMILIAR_ENABLE_WRITES on (stdio) / off (HTTP) true allows guarded writes; over HTTP it's off unless set.

/healthz is an unauthenticated liveness probe (used by the Docker healthcheck and reverse-proxy probes). It returns only {"status":"ok","server":"familiar","version":"…"} — no account data. Every MCP call under FAMILIAR_HTTP_PATH requires authentication.

One-time login (why it happens on the host)

EVE SSO login is a browser PKCE flow with a localhost callback — awkward inside a container. Authenticate once on a machine with a browser, then hand the resulting token store to the container. The container only ever refreshes tokens.

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\

Headless host? Use uv run familiar login --no-browser: it prints the SSO URL, you authorize in any browser, and paste the redirected callback URL back on stdin. The /data volume is mounted read-write so token refresh (which rotates the refresh token) and the audit log persist.

Published images (Gitea)

.gitea/workflows/docker.yml builds, smoke-tests (non-root, /healthz 200, MCP 401 without / 200 with a token), and pushes on every push to main/dev:

Branch Tags
main git.hiddenden.cafe/hiddenden/familiar:latest, :main, :sha-<12>
dev git.hiddenden.cafe/hiddenden/familiar:dev, :sha-<12>

Pushing needs a Gitea PAT with write:package scope stored as the repo secret REGISTRY_TOKEN. Pick the channel with FAMILIAR_IMAGE in .env.

Run with Docker Compose

cp .env.example .env
# edit .env: FAMILIAR_HTTP_TOKEN (openssl rand -hex 32),
#            FAMILIAR_IMAGE (:latest prod / :dev dev channel)

docker network create proxy    # once, if your reverse proxy doesn't own it yet

docker login git.hiddenden.cafe
docker compose pull && docker compose up -d
#   ...or build locally instead:  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 with a read_only rootfs + tmpfs /tmp, no-new-privileges and cap_drop: ALL, and healthchecks GET /healthz.

TLS reverse proxy

familiar terminates no TLS itself — a proxy does. Streamable HTTP holds a long-lived response open, so disable buffering and use a generous read timeout on the MCP path.

nginx

location /mcp {
    proxy_pass http://127.0.0.1:8080/mcp;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;          # stream events straight through
    proxy_read_timeout 3600s;     # don't cut long-lived MCP sessions
}

(If you run the container on the proxy network instead of publishing to the host, point proxy_pass at http://familiar:8080/mcp.)

Caddy

familiar.yourdomain.tld {
    reverse_proxy familiar:8080
}

Caddy fetches and renews the certificate automatically and streams by default.

Pangolin

The compose service exposes :8080 on the shared external proxy network and carries no proxy-specific labels. In Pangolin, add a resource for familiar that forwards to http://familiar:8080 (target the container's /mcp path), and let Pangolin terminate HTTPS. Make sure Pangolin's own container is attached to the same proxy network so it can reach the service by name. Your public URL is then https://familiar.yourdomain.tld/mcp.

Because streamable HTTP holds a long-lived response open, raise the read/response timeout on the familiar resource and disable response buffering if the option is exposed (same reasoning as the nginx proxy_read_timeout / proxy_buffering off above).

Operational notes

  • Rotate the token by changing FAMILIAR_HTTP_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 /data/audit.log for the write trail.
  • Re-login (host) whenever you change requested scopes, then refresh the /data token store.