Files
familiar/docs/deployment.md
T
Latte 904e96a559
docker / lint (push) Successful in 50s
docker / test (push) Successful in 58s
CI / lint (pull_request) Successful in 50s
CI / test (pull_request) Successful in 1m1s
CI / publish (pull_request) Has been skipped
docker / lint (pull_request) Successful in 52s
docker / docker (push) Successful in 43s
docker / test (pull_request) Successful in 59s
docker / docker (pull_request) Successful in 37s
ci(docker): publish image to Gitea registry (dev + main), mirror AegisGitea-MCP
Replace the opt-in docker workflow with the AegisGitea-MCP-style pipeline:
lint -> test -> build -> smoke-test -> push to git.hiddenden.cafe.

- Image git.hiddenden.cafe/hiddenden/familiar; main -> :latest + :main, dev ->
  :dev, every build also tagged :sha-<12>. Push gated to main/dev push events,
  authenticated with the REGISTRY_TOKEN secret; scoped cleanup after each run.
- docker-compose.yml pulls the published image (FAMILIAR_IMAGE, default :latest)
  with local build as a fallback; .env.example + docs/deployment.md document the
  channels and the pull-based deploy.
2026-07-03 20:17:44 +02:00

5.7 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.

Published images (Gitea)

.gitea/workflows/docker.yml builds, smoke-tests, and pushes the image to the Gitea container registry 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 (the auto GITEA_TOKEN lacks package-write here). Pick the channel with FAMILIAR_IMAGE in .env (:latest/:main for prod, :dev).

Run with Docker Compose

cp .env.example .env
# edit .env: FAMILIAR_HTTP_AUTH_TOKEN (strong!), FAMILIAR_DOMAIN, cert resolver,
#            FAMILIAR_IMAGE (:latest for prod, :dev for the dev channel)
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

# Pull the published image (prod):
docker login git.hiddenden.cafe
docker compose pull && docker compose up -d

# ...or build it 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, 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.