Files
ai-agent/Dockerfile
Gabriel Vidal 3a0ef08dcf feat(worker): sidecar runs as a paired macOS worker — feed, pairing, launchd installer
- liveness falls back to psutil where there is no /proc (macOS)
- GET /feed + /feed/file: the hub pulls transcripts it has no mount for
- POST /pair trades a one-time code for the worker's bearer; /unpair drops it
- /health reports worker identity + permission mode
- worker/install-macos.sh: venv, launchd agent bound to the Tailscale IP,
  prints the pairing string (addr/code/claude login)
- Dockerfile copies every sidecar module (claude_cli was missing, so the
  in-container runner could not import)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 13:49:07 +02:00

73 lines
4.0 KiB
Docker

# syntax=docker/dockerfile:1
# BuildKit `--mount=type=cache` steps below persist the npm/pip/vite/apt caches
# across builds (survive layer-cache misses), so a dependency bump or a pruned
# builder re-installs from the local cache instead of re-downloading everything.
# Requires BuildKit (default with `docker compose build` / buildx here).
# ── stage 1: build the React PWA frontend ───────────────────────────────────
FROM node:20-alpine AS frontend
WORKDIR /build
COPY frontend/package.json frontend/package-lock.json* frontend/.npmrc ./
# @gabvdl/ui now comes from the homelab private registry (verdaccio) instead of a
# vendored tarball. The .npmrc scopes @gabvdl → http://localhost:4873; this build
# stage runs with `network: host` (see docker-compose.yml) so that localhost is
# the host-bound verdaccio. `npm ci` is deterministic (installs exactly the
# lockfile) and faster than `npm install`; the cache mount keeps the download
# cache warm across builds so a lockfile change re-links from cache instead of
# re-fetching all ~250 MB of deps.
RUN --mount=type=cache,target=/root/.npm npm ci
COPY frontend/ ./
# Persist Vite's esbuild dep pre-bundle cache across builds. (The app bundle
# itself is re-rollup'd on every source change — that ~17s is inherent to a
# production build and no cache avoids it; this mount only saves the dep
# optimize pass on cold builds.)
RUN --mount=type=cache,target=/build/node_modules/.vite npm run build
# ── stage 2: FastAPI backend serving the API + the built frontend ───────────
FROM python:3.12-slim
WORKDIR /app
# git: read each project's last-commit date / recent commits from the mounted repo.
# curl/ca-certificates: fetch the Claude Code CLI below. ripgrep: the CLI's search.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get install -y --no-install-recommends git curl ca-certificates ripgrep
# ── the in-image runner: the Claude Code CLI itself ─────────────────────────
# GOAL.md's first gap is folding the host sidecar into the container. Claude Code
# ships as a self-contained native binary, so the image can just carry it: with
# RUNNER_IN_CONTAINER=1 the entrypoint runs sidecar/sidecar.py against this binary
# on the container's loopback, and the image can spawn sessions with no host
# process at all. The homelab keeps using its host sidecar (nothing changes there).
#
# The installer drops a versioned binary under $HOME/.local/share/claude and
# symlinks it; we resolve the symlink into /usr/local/bin so the CLI is on PATH
# for whatever uid the container is later told to run as.
ARG CLAUDE_VERSION=stable
RUN --mount=type=cache,target=/root/.cache/claude-install \
HOME=/tmp/claude-install sh -c \
"curl -fsSL https://claude.ai/install.sh | bash -s -- ${CLAUDE_VERSION}" \
&& cp -L /tmp/claude-install/.local/bin/claude /usr/local/bin/claude \
&& chmod 0755 /usr/local/bin/claude \
&& rm -rf /tmp/claude-install \
&& claude --version
# Deps first, keyed on requirements.txt only, so editing backend code below never
# re-resolves pip. Cache mount keeps wheels warm across dep bumps / pruned builds.
COPY backend/requirements.txt /app/requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r /app/requirements.txt
COPY backend/ /app/
# The runner: the same FastAPI wrapper the homelab runs on the host, launched
# in-container by the entrypoint when RUNNER_IN_CONTAINER=1 (dead weight otherwise
# — it shares the backend's fastapi/uvicorn, so it costs no extra deps).
COPY sidecar/sidecar.py sidecar/claude_cli.py sidecar/feed.py sidecar/pairing.py /app/sidecar/
COPY --chmod=0755 docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
# Built PWA assets served as static files at the web root.
COPY --from=frontend /build/dist /app/static
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]