GOAL.md's first gap: without a runner a standalone image is a read-only viewer, because launching `claude -p` needed a host-side process. The Claude Code CLI is a self-contained native binary, so the image can just carry it — and the *same* sidecar/sidecar.py the homelab runs on its host runs in-container against it. With RUNNER_IN_CONTAINER=1 (the standalone default) docker-entrypoint.sh starts the runner on the loopback, mints a SIDECAR_TOKEN if none was given, overrides SIDECAR_URL to point at it, and gives the CLI a writable $HOME on the data volume. The transcripts it writes there become a third live source (RUNNER_TRANSCRIPTS_DIR → SOURCE_DIRS), so an in-container session streams into the viewer like any other. Verified end to end: a standalone container (workspace + data, no homelab, no host sidecar) spawns a session, the CLI runs it, and the turn renders in the conversation list with its model tag. The CLI authenticates from ANTHROPIC_API_KEY or a Claude home mounted at RUNNER_HOME. The homelab is unchanged: it leaves the flag off and keeps its host sidecar, which is what lets a run use the host's own hooks, skills and credentials. standalone-smoke.sh now also asserts the CLI is on PATH and the runner is healthy — the packaging property that would otherwise regress silently. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73 lines
4.0 KiB
Docker
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 /app/sidecar/sidecar.py
|
|
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"]
|