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.
This commit is contained in:
13 files changed
+727
-4
No files matched your search
@@ -0,0 +1,58 @@
|
||||
# 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"]
|
||||
Reference in new issue
Block a user