# ---------- build the frontend ---------- FROM node:22-bookworm AS web WORKDIR /web COPY web/package.json ./ RUN npm install COPY web/ ./ RUN npm run build # ---------- runtime ---------- # Node 22+: required by OpenClaw (22.19+); everything else is version-agnostic. FROM node:22-bookworm AS runtime # System deps: tmux (session durability), git, build tools (node-pty native build), # ripgrep (used by the coding CLIs), curl/ca-certs. RUN apt-get update && apt-get install -y --no-install-recommends \ tmux git ca-certificates curl python3 make g++ ripgrep bubblewrap rsync \ && rm -rf /var/lib/apt/lists/* ENV LANG=C.UTF-8 # AI coding CLIs available to every session (installed globally, on PATH for all users). # Pinned to @latest so a factory reboot (no-cache rebuild) reinstalls the newest # published versions — that's what the "Relaunch & update" button triggers. RUN npm install -g @anthropic-ai/claude-code@latest @openai/codex@latest # Newer agents, best-effort so a publish hiccup can't break the image build; # the app marks any missing binary "unavailable" gracefully. RUN npm install -g @google/gemini-cli@latest || echo "gemini-cli install failed" RUN npm install -g opencode-ai@latest || echo "opencode install failed" RUN npm install -g openclaw@latest || echo "openclaw install failed" # ccusage powers the Usage page (token/cost aggregation across agents). Its # platform binary ships without an execute bit and the package tries to chmod # itself on first run — which fails with EPERM at runtime as the non-root user. # Make it executable here (as root) so it just works for the node user. RUN npm install -g ccusage \ && find "$(npm root -g)/ccusage" -type f -name ccusage -exec chmod a+rx {} + \ || echo "ccusage install failed" # uv: fast Python package/env manager agents use to (re)build project envs from # their lockfiles on local disk. Installed to /usr/local/bin (on PATH for all). RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh \ || echo "uv install failed" # hf: the Hugging Face CLI (auth, hub up/downloads). Installed as a uv tool into # a world-readable dir with its shim on the global PATH; picks up an HF_TOKEN # Space secret automatically. RUN env UV_TOOL_BIN_DIR=/usr/local/bin UV_TOOL_DIR=/opt/uv-tools \ uv tool install --python /usr/bin/python3 "huggingface_hub[cli]" \ || echo "hf cli install failed" # Login shells source /etc/profile, which RESETS PATH — dropping the build-time # ~/.local/bin and the user-install dirs under $AM_LOCAL (pip --user, npm # prefix). profile.d runs after that reset, so restore them here. RUN printf '%s\n' \ 'PATH="/home/node/.local/bin:$PATH"' \ '[ -n "$AM_LOCAL" ] && PATH="$AM_LOCAL/py/bin:$AM_LOCAL/npm/bin:$AM_LOCAL/bin:$PATH"' \ 'export PATH' \ > /etc/profile.d/agent-manager.sh # Non-root user: the node base image already ships uid 1000 as "node" (HF runs as uid 1000). ENV HOME=/home/node ENV PATH=/home/node/.local/bin:$PATH WORKDIR /app RUN chown node:node /app # Server deps first (better layer caching); node-pty compiles here. COPY --chown=node:node server/package.json server/ USER node RUN cd server && npm install --omit=dev # Best-effort Hermes (Nous Research). Hardened so it can never hang or fail the # build; the app marks it "unavailable" gracefully if the binary isn't on PATH. RUN (curl -fsSL https://hermes-agent.nousresearch.com/install.sh -o /tmp/h.sh \ && timeout 180 bash /tmp/h.sh /app/build-env-keys.txt EXPOSE 7860 CMD ["sh", "/app/entrypoint.sh"]