g1t/services/runner/base/Dockerfile
| 1 | # syntax=docker/dockerfile:1.7 |
| 2 | # |
| 3 | # g1t-runner-base: everything a g1t sandbox has apart from the g1t runner |
| 4 | # itself. Agents, checks, the merge queue, workflow jobs and g1t.page |
| 5 | # builds all run in it: git, Node, Python, Go, Rust (with the formatter, |
| 6 | # the linter and the wasm32 target), the usual build tools, and the |
| 7 | # Claude Code CLI. |
| 8 | # |
| 9 | # Built and pushed by `node scripts/deploy.mjs build-base` (by hand, or by |
| 10 | # .g1t/workflows/runner-base.yml), which records what it pushed in |
| 11 | # services/runner/base.json. The runner's own image |
| 12 | # (services/runner/Dockerfile) is this plus one file, so a change to the |
| 13 | # runner builds in seconds. docs/DEPLOYING.md explains the two. |
| 14 | # |
| 15 | # Layers go from what changes least to what changes most: the system's |
| 16 | # packages, then Go, then Rust, then the Claude Code CLI on top, so a new |
| 17 | # CLI version rebuilds and pushes one layer. |
| 18 | # |
| 19 | # Build context: this folder (nothing is copied from it). |
| 20 | |
| 21 | FROM node:24-bookworm-slim |
| 22 | |
| 23 | # Docs, man pages and translations are never read in a sandbox; dpkg |
| 24 | # leaves them out of every package installed from here on (copyright |
| 25 | # files stay). Downloaded packages are kept for the build's apt cache. |
| 26 | RUN printf '%s\n' \ |
| 27 | 'path-exclude=/usr/share/doc/*' \ |
| 28 | 'path-include=/usr/share/doc/*/copyright' \ |
| 29 | 'path-exclude=/usr/share/man/*' \ |
| 30 | 'path-exclude=/usr/share/info/*' \ |
| 31 | 'path-exclude=/usr/share/groff/*' \ |
| 32 | 'path-exclude=/usr/share/lintian/*' \ |
| 33 | 'path-exclude=/usr/share/linda/*' \ |
| 34 | 'path-exclude=/usr/share/locale/*' \ |
| 35 | 'path-include=/usr/share/locale/locale.alias' \ |
| 36 | > /etc/dpkg/dpkg.cfg.d/01-g1t-slim \ |
| 37 | && rm -f /etc/apt/apt.conf.d/docker-clean \ |
| 38 | && echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-downloads |
| 39 | |
| 40 | # The system's packages. The apt cache and package lists live in |
| 41 | # BuildKit's cache, not the image (run `sudo apt-get update` before |
| 42 | # installing more). dpkg skips its fsync after every file, which an image |
| 43 | # build has no use for and which made this step several times slower. |
| 44 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ |
| 45 | --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ |
| 46 | apt-get update \ |
| 47 | && apt-get install -y --no-install-recommends -o Dpkg::Options::=--force-unsafe-io \ |
| 48 | git ca-certificates curl build-essential pkg-config libssl-dev \ |
| 49 | python3 python3-pip python3-venv ripgrep jq zstd \ |
| 50 | sudo unzip zip xz-utils wget file gnupg lsb-release openssh-client \ |
| 51 | && rm -rf /var/log/apt /var/log/dpkg.log /var/cache/debconf/*-old |
| 52 | |
| 53 | # Workflows expect GitHub's runner layout under /home/runner, and sudo |
| 54 | # without a password. The agent works in /work. |
| 55 | RUN mkdir /work && chown node:node /work \ |
| 56 | && mkdir -p /home/runner/work /home/runner/_temp /home/runner/_tool /home/runner/_actions \ |
| 57 | && chown -R node:node /home/runner \ |
| 58 | && echo 'node ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/node \ |
| 59 | && chmod 0440 /etc/sudoers.d/node |
| 60 | |
| 61 | # Go, from go.dev (Debian's is years behind), without its own test suite |
| 62 | # and API history, which only Go's developers use. |
| 63 | ARG GO_VERSION=1.27.1 |
| 64 | ARG GO_SHA256=63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445 |
| 65 | RUN curl -fsSLo /tmp/go.tgz "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" \ |
| 66 | && echo "${GO_SHA256} /tmp/go.tgz" | sha256sum -c - \ |
| 67 | && tar -C /usr/local -xzf /tmp/go.tgz \ |
| 68 | && rm -rf /tmp/go.tgz /usr/local/go/test /usr/local/go/api /usr/local/go/doc \ |
| 69 | && ln -s /usr/local/go/bin/go /usr/local/go/bin/gofmt /usr/local/bin/ |
| 70 | |
| 71 | # Claude Code refuses to skip permission prompts as root. |
| 72 | USER node |
| 73 | ENV HOME=/home/node |
| 74 | ENV PATH=/home/node/.cargo/bin:/home/node/go/bin:$PATH |
| 75 | |
| 76 | # Rust stable, for the user the agent runs as, with the formatter and |
| 77 | # linter that CI so often checks with (an agent that cannot run them only |
| 78 | # finds out from a failed workflow), and the wasm32 target that Workers, |
| 79 | # and g1t's own deploys, build for. |
| 80 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ |
| 81 | | sh -s -- -y --no-modify-path --profile minimal --default-toolchain stable \ |
| 82 | --component rustfmt --component clippy --target wasm32-unknown-unknown \ |
| 83 | && rm -rf /home/node/.rustup/downloads /home/node/.rustup/tmp \ |
| 84 | /home/node/.rustup/toolchains/*/share/doc \ |
| 85 | /home/node/.rustup/toolchains/*/share/man |
| 86 | |
| 87 | # Commits are the g1t agent's; the harness does not sign them as its own. |
| 88 | RUN mkdir -p /home/node/.claude \ |
| 89 | && echo '{"includeCoAuthoredBy": false}' > /home/node/.claude/settings.json |
| 90 | |
| 91 | # The Claude Code CLI, pinned, on top: it changes most often of anything |
| 92 | # here, and is one layer to rebuild and push. |
| 93 | ARG CLAUDE_CODE_VERSION=2.1.291 |
| 94 | USER root |
| 95 | # npm's cache stays in BuildKit's cache, out of the image. |
| 96 | RUN --mount=type=cache,target=/tmp/npm-cache \ |
| 97 | npm install --global --no-audit --no-fund --cache /tmp/npm-cache "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \ |
| 98 | && claude --version |
| 99 | USER node |