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.
7.5 KiB
Deployment — familiar as a remote MCP server
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 getting-started.
- 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 barefamiliar) starts HTTP. - Loopback bind by default (
127.0.0.1). The Docker image binds0.0.0.0so 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 explicitFAMILIAR_ALLOW_INSECURE_BIND=trueescape 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 inFAMILIAR_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_TOKENand restarting; update every client header. - Restrict inbound at the proxy/firewall to the clients you expect.
- Watch
docker compose logsfor401auth failures and/data/audit.logfor the write trail. - Re-login (host) whenever you change requested scopes, then refresh the
/datatoken store.