Files
familiar/docker/Dockerfile
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

59 lines
2.0 KiB
Docker

# syntax=docker/dockerfile:1
# --------------------------------------------------------------------------- #
# Build stage: build a wheel from the source tree with uv.
# --------------------------------------------------------------------------- #
FROM python:3.12-slim AS builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
RUN pip install --no-cache-dir uv
# Files needed to build the wheel (pyproject references README.md + LICENSE).
COPY pyproject.toml README.md LICENSE ./
COPY src ./src
RUN uv build --wheel --out-dir /dist
# --------------------------------------------------------------------------- #
# Runtime stage: minimal, non-root, wheel installed into a clean environment.
# --------------------------------------------------------------------------- #
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
# HTTP transport, reachable from the proxy network, token-gated.
FAMILIAR_TRANSPORT=http \
FAMILIAR_HTTP_HOST=0.0.0.0 \
FAMILIAR_HTTP_PORT=8000 \
FAMILIAR_HTTP_PATH=/mcp \
# Token store + audit log + write toggle live here — mount a persistent volume.
FAMILIAR_DATA_DIR=/data \
# Public instance: writes off by default (smaller blast radius if the token leaks).
FAMILIAR_ENABLE_WRITES=false
WORKDIR /app
# Non-root runtime user; /data owned by it so token refresh can write.
RUN useradd -m -u 1000 -s /usr/sbin/nologin familiar \
&& mkdir -p /data \
&& chown -R familiar:familiar /data
COPY --from=builder /dist/*.whl /tmp/
RUN pip install --no-cache-dir /tmp/*.whl && rm -f /tmp/*.whl
USER familiar
EXPOSE 8000
VOLUME ["/data"]
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD python -c "import os,httpx; httpx.get(f\"http://127.0.0.1:{os.environ.get('FAMILIAR_HTTP_PORT','8000')}/healthz\", timeout=5).raise_for_status()" || exit 1
CMD ["familiar", "serve"]