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