/scaffold runs the owner's project generator (SIDECAR_SCAFFOLD_CMD, default the lab's new-project) with a vars JSON and reads its SCAFFOLD_RESULT= line back (a generated site password stays out of the log); /scaffold/check is its --check. /relocate lets a session go on in another directory: it stops a live run and copies the session file (same id) into that cwd's transcript folder, where the next --resume finds it with its whole history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
4.7 KiB
Docker
84 lines
4.7 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).
|
|
# Every module it imports is listed: a missing one (outbox, skills_list and
|
|
# stop_guard once were) crash-loops the runner on import, which only
|
|
# scripts/standalone-smoke.sh would notice — the homelab runs the host sidecar.
|
|
COPY sidecar/sidecar.py sidecar/config.py sidecar/pids.py sidecar/validate.py \
|
|
sidecar/claude_accounts.py sidecar/launch.py sidecar/fork.py \
|
|
sidecar/routes_runs.py sidecar/routes_accounts.py \
|
|
sidecar/claude_cli.py sidecar/feed.py sidecar/pairing.py \
|
|
sidecar/outbox.py sidecar/skills_list.py sidecar/stop_guard.py \
|
|
sidecar/shared_skills.py sidecar/scaffold.py /app/sidecar/
|
|
# The notify CLI (push / ask / form through the hub) the runner appends to each
|
|
# run's PATH — sidecar/config.py's CLI_DIR, i.e. ../cli from the sidecar.
|
|
COPY cli/ /app/cli/
|
|
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"]
|