Files
beaver-gateway/Dockerfile
T

94 lines
3.7 KiB
Docker

# syntax=docker/dockerfile:1.7
#
# Two stages: a uv-based builder that resolves the venv, and a thin
# python:3.13-slim runtime with bun + the `claude` CLI baked in.
#
# Alpine is intentionally avoided: @anthropic-ai/claude-code ≥ 2.1.113
# ships a glibc-only native binary (anthropics/claude-code#50270).
#
# This image is deliberately un-opinionated about ports and command —
# the consumer's compose file decides what to publish and how to invoke.
# ---- Builder: uv + Python deps ----
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
UV_PYTHON_DOWNLOADS=never
# `git` is required at build time: the `prod` extra resolves
# raycast-api / claude-code-api from git URLs.
RUN apt-get update \
&& apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-dev --no-install-project --extra prod
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --extra prod
# ---- UI: SvelteKit SPA built with bun ----
FROM oven/bun:1-slim AS ui
WORKDIR /ui
COPY ui/package.json ui/bun.lock ./
RUN bun install --frozen-lockfile
COPY ui ./
RUN bun --bun svelte-kit sync && bun --bun run build
# ---- Runtime ----
FROM python:3.13-slim AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
# Bun native binary (glibc): `bunx` runs the stdio MCP servers declared in
# the user's config.
COPY --from=oven/bun:1-slim /usr/local/bin/bun /usr/local/bin/bun
RUN ln -s /usr/local/bin/bun /usr/local/bin/bunx
# The claude-agent-sdk wheel bundles a CLI, but it trails the npm release:
# 0.2.157 carries 2.1.277, which does not know claude-opus-5-5 and writes an
# [claude-code:unrecognized_model] warning per turn. Install the npm build and
# point BEAVER_CLAUDE_CLI at it; the adapter prefers it over the bundled one.
#
# Only the glibc platform package is pulled: the meta package also drags in the
# musl build, and this image is glibc for the reason stated at the top. The
# tree lands under /opt world-readable because the model runs as beaver-runner,
# not as root.
ARG CLAUDE_CODE_VERSION=2.1.280
RUN mkdir -p /opt/claude-code \
&& cd /opt/claude-code \
&& echo '{"name":"claude-cli-holder","private":true}' > package.json \
&& bun add @anthropic-ai/claude-code-linux-x64@${CLAUDE_CODE_VERSION} \
&& ln -sf /opt/claude-code/node_modules/@anthropic-ai/claude-code-linux-x64/claude \
/usr/local/bin/claude-cli \
&& chmod -R a+rX /opt/claude-code \
&& /usr/local/bin/claude-cli --version
ENV BEAVER_CLAUDE_CLI=/usr/local/bin/claude-cli
# The model process runs as `beaver-runner`, not as the gateway: the
# adapter spawns claude under this uid with a whitelisted environment.
# `acl` lets the entrypoint grant it write access to the vault sub-mounts
# without chowning files that Obsidian Sync keeps rewriting as root.
RUN apt-get update \
&& apt-get install -y --no-install-recommends acl sudo \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --uid 1001 --create-home --shell /usr/sbin/nologin beaver-runner
ENV PATH=/app/.venv/bin:$PATH
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app /app
COPY --from=ui /ui/build /app/ui/build
COPY --chmod=0755 docker/secret /usr/local/bin/secret
COPY --chmod=0755 docker/beaver-secret /usr/local/libexec/beaver-secret
COPY --chmod=0440 docker/sudoers /etc/sudoers.d/beaver-secret
RUN visudo -cf /etc/sudoers.d/beaver-secret
WORKDIR /app
ENTRYPOINT ["python", "-m"]
CMD ["beaver_gateway"]