| 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 and musl targets), Java (Temurin 21), .NET |
| 7 | # (SDK 8), Ruby (3.3), the usual build tools, Docker (Engine, Buildx, |
| 8 | # Compose), and the Claude Code CLI. |
| 9 | # |
| 10 | # Built and pushed by `node scripts/deploy.mjs build-base` (by hand, or by |
| 11 | # .g1t/workflows/runner-base.yml), which records what it pushed in |
| 12 | # services/runner/base.json. The runner's own image |
| 13 | # (services/runner/Dockerfile) is this plus one file, so a change to the |
| 14 | # runner builds in seconds. docs/DEPLOYING.md explains the two. |
| 15 | # |
| 16 | # Layers go from what changes least to what changes most: the system's |
| 17 | # packages, Docker, then Go, then Rust, then Java, .NET and Ruby, then the |
| 18 | # Claude Code CLI on top, so a new CLI version rebuilds and pushes one |
| 19 | # layer. |
| 20 | # |
| 21 | # Java and Ruby live in the runner's tool cache (RUNNER_TOOL_CACHE, |
| 22 | # /home/runner/_tool), laid out as the setup actions look for them, so |
| 23 | # `actions/setup-java` (temurin, 21) and `ruby/setup-ruby` (3.3) find them |
| 24 | # there instead of downloading. .NET is in /usr/share/dotnet, where |
| 25 | # `actions/setup-dotnet` installs, so it finds the SDK already there. |
| 26 | # |
| 27 | # Build context: this folder (nothing is copied from it). |
| 28 | |
| 29 | FROM node:24-bookworm-slim |
| 30 | |
| 31 | # Docs, man pages and translations are never read in a sandbox; dpkg |
| 32 | # leaves them out of every package installed from here on (copyright |
| 33 | # files stay). Downloaded packages are kept for the build's apt cache. |
| 34 | RUN printf '%s\n' \ |
| 35 | 'path-exclude=/usr/share/doc/*' \ |
| 36 | 'path-include=/usr/share/doc/*/copyright' \ |
| 37 | 'path-exclude=/usr/share/man/*' \ |
| 38 | 'path-exclude=/usr/share/info/*' \ |
| 39 | 'path-exclude=/usr/share/groff/*' \ |
| 40 | 'path-exclude=/usr/share/lintian/*' \ |
| 41 | 'path-exclude=/usr/share/linda/*' \ |
| 42 | 'path-exclude=/usr/share/locale/*' \ |
| 43 | 'path-include=/usr/share/locale/locale.alias' \ |
| 44 | > /etc/dpkg/dpkg.cfg.d/01-g1t-slim \ |
| 45 | && rm -f /etc/apt/apt.conf.d/docker-clean \ |
| 46 | && echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-downloads |
| 47 | |
| 48 | # The system's packages. The apt cache and package lists live in |
| 49 | # BuildKit's cache, not the image (run `sudo apt-get update` before |
| 50 | # installing more). dpkg skips its fsync after every file, which an image |
| 51 | # build has no use for and which made this step several times slower. |
| 52 | # musl-tools is musl-gcc, with which (and `rustup target add |
| 53 | # x86_64-unknown-linux-musl`) the static g1t runner builds in a workflow |
| 54 | # job (scripts/build-runner.mjs). |
| 55 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ |
| 56 | --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ |
| 57 | apt-get update \ |
| 58 | && apt-get install -y --no-install-recommends -o Dpkg::Options::=--force-unsafe-io \ |
| 59 | git ca-certificates curl build-essential pkg-config libssl-dev \ |
| 60 | python3 python3-pip python3-venv ripgrep jq zstd \ |
| 61 | sudo unzip zip xz-utils wget file gnupg lsb-release openssh-client \ |
| 62 | musl-tools \ |
| 63 | && rm -rf /var/log/apt /var/log/dpkg.log /var/cache/debconf/*-old |
| 64 | |
| 65 | # Docker, for workflow jobs: the CLI with Buildx and Compose, and the |
| 66 | # Engine (dockerd, containerd, runc), which the runner starts as a job's |
| 67 | # own the first time the job uses it (crates/runner/src/docker). From |
| 68 | # Docker's own repository, its signing key checked against the fingerprint |
| 69 | # Docker publishes. Nothing here runs until a job asks for it. |
| 70 | ARG DOCKER_KEY_FINGERPRINT=9DC858229FC7DD38854AE2D88D81803C0EBFCD88 |
| 71 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ |
| 72 | --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ |
| 73 | install -m 0755 -d /etc/apt/keyrings \ |
| 74 | && curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \ |
| 75 | && GNUPGHOME="$(mktemp -d)" gpg --show-keys --with-colons /etc/apt/keyrings/docker.asc \ |
| 76 | | grep -q "^fpr:::::::::${DOCKER_KEY_FINGERPRINT}:" \ |
| 77 | && echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" \ |
| 78 | > /etc/apt/sources.list.d/docker.list \ |
| 79 | && apt-get update \ |
| 80 | && apt-get install -y --no-install-recommends -o Dpkg::Options::=--force-unsafe-io \ |
| 81 | docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \ |
| 82 | && rm -rf /var/log/apt /var/log/dpkg.log /var/cache/debconf/*-old \ |
| 83 | && docker --version && dockerd --version && docker buildx version && docker compose version |
| 84 | |
| 85 | # Workflows expect GitHub's runner layout under /home/runner, and sudo |
| 86 | # without a password. The agent works in /work. |
| 87 | RUN mkdir /work && chown node:node /work \ |
| 88 | && mkdir -p /home/runner/work /home/runner/_temp /home/runner/_tool /home/runner/_actions \ |
| 89 | && chown -R node:node /home/runner \ |
| 90 | && echo 'node ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/node \ |
| 91 | && chmod 0440 /etc/sudoers.d/node |
| 92 | |
| 93 | # Go, from go.dev (Debian's is years behind), without its own test suite |
| 94 | # and API history, which only Go's developers use. |
| 95 | ARG GO_VERSION=1.27.1 |
| 96 | ARG GO_SHA256=63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445 |
| 97 | RUN curl -fsSLo /tmp/go.tgz "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" \ |
| 98 | && echo "${GO_SHA256} /tmp/go.tgz" | sha256sum -c - \ |
| 99 | && tar -C /usr/local -xzf /tmp/go.tgz \ |
| 100 | && rm -rf /tmp/go.tgz /usr/local/go/test /usr/local/go/api /usr/local/go/doc \ |
| 101 | && ln -s /usr/local/go/bin/go /usr/local/go/bin/gofmt /usr/local/bin/ |
| 102 | |
| 103 | # Claude Code refuses to skip permission prompts as root. |
| 104 | USER node |
| 105 | ENV HOME=/home/node |
| 106 | ENV PATH=/home/node/.cargo/bin:/home/node/go/bin:$PATH |
| 107 | |
| 108 | # Rust stable, for the user the agent runs as, with the formatter and |
| 109 | # linter that CI so often checks with (an agent that cannot run them only |
| 110 | # finds out from a failed workflow), and the wasm32 target that Workers, |
| 111 | # and g1t's own deploys, build for. |
| 112 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ |
| 113 | | sh -s -- -y --no-modify-path --profile minimal --default-toolchain stable \ |
| 114 | --component rustfmt --component clippy --target wasm32-unknown-unknown \ |
| 115 | && rm -rf /home/node/.rustup/downloads /home/node/.rustup/tmp \ |
| 116 | /home/node/.rustup/toolchains/*/share/doc \ |
| 117 | /home/node/.rustup/toolchains/*/share/man |
| 118 | |
| 119 | USER root |
| 120 | |
| 121 | # What Java, .NET and Ruby need from the system: ICU for .NET, and the |
| 122 | # headers that gems with native extensions compile against (Ruby's own |
| 123 | # were built with them). |
| 124 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ |
| 125 | --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ |
| 126 | apt-get update \ |
| 127 | && apt-get install -y --no-install-recommends -o Dpkg::Options::=--force-unsafe-io \ |
| 128 | libicu72 libyaml-dev libffi-dev zlib1g-dev libgmp-dev libreadline-dev \ |
| 129 | && rm -rf /var/log/apt /var/log/dpkg.log /var/cache/debconf/*-old |
| 130 | |
| 131 | # Java: Eclipse Temurin 21 (LTS), from Adoptium, in the tool cache under |
| 132 | # the name `actions/setup-java` gives it (Adoptium's semver with `+` |
| 133 | # written as `-`), with the `x64.complete` marker it checks for. The JDK's |
| 134 | # source archive is left out. |
| 135 | ARG TEMURIN_RELEASE=jdk-21.0.12.1+1 |
| 136 | ARG TEMURIN_TOOLCACHE_VERSION=21.0.12-101.0.LTS |
| 137 | ARG TEMURIN_SHA256=ce79869e1307ed8ee1e2baa86a412b1eb5b75d10a01006d788a6f968bcfaee94 |
| 138 | RUN file="$(echo "${TEMURIN_RELEASE#jdk-}" | tr + _)" \ |
| 139 | && tag="$(echo "${TEMURIN_RELEASE}" | sed 's/+/%2B/')" \ |
| 140 | && curl -fsSLo /tmp/jdk.tgz "https://github.com/adoptium/temurin21-binaries/releases/download/${tag}/OpenJDK21U-jdk_x64_linux_hotspot_${file}.tar.gz" \ |
| 141 | && echo "${TEMURIN_SHA256} /tmp/jdk.tgz" | sha256sum -c - \ |
| 142 | && dir="/home/runner/_tool/Java_Temurin-Hotspot_jdk/${TEMURIN_TOOLCACHE_VERSION}" \ |
| 143 | && mkdir -p "$dir/x64" \ |
| 144 | && tar -C "$dir/x64" --strip-components=1 -xzf /tmp/jdk.tgz \ |
| 145 | && rm -f /tmp/jdk.tgz "$dir/x64/lib/src.zip" \ |
| 146 | && touch "$dir/x64.complete" \ |
| 147 | && chown -R node:node /home/runner/_tool |
| 148 | ENV JAVA_HOME=/home/runner/_tool/Java_Temurin-Hotspot_jdk/${TEMURIN_TOOLCACHE_VERSION}/x64 |
| 149 | ENV JAVA_HOME_21_X64=${JAVA_HOME} |
| 150 | |
| 151 | # .NET: the SDK 8 (LTS), from Microsoft's builds, checked against the |
| 152 | # SHA-512 in its release metadata. In /usr/share/dotnet, owned by the |
| 153 | # job's user, so `actions/setup-dotnet` can add other SDKs beside it. The |
| 154 | # SDK's translated messages (about 100 MB) are left out: it speaks English. |
| 155 | ARG DOTNET_SDK_VERSION=8.0.425 |
| 156 | ARG DOTNET_SDK_SHA512=934b8060a7190e5909ad1fd0785db542f487b3bbf6cdd14826b02095fdd0d0394298b1634085eff302928fccc33f7c1a7253e9b87df555fc36fce819bcd2e798 |
| 157 | RUN curl -fsSLo /tmp/dotnet.tgz "https://builds.dotnet.microsoft.com/dotnet/Sdk/${DOTNET_SDK_VERSION}/dotnet-sdk-${DOTNET_SDK_VERSION}-linux-x64.tar.gz" \ |
| 158 | && echo "${DOTNET_SDK_SHA512} /tmp/dotnet.tgz" | sha512sum -c - \ |
| 159 | && mkdir -p /usr/share/dotnet \ |
| 160 | && tar -C /usr/share/dotnet -xzf /tmp/dotnet.tgz \ |
| 161 | && rm /tmp/dotnet.tgz \ |
| 162 | && find /usr/share/dotnet/sdk -type d \( -name cs -o -name de -o -name es -o -name fr -o -name it \ |
| 163 | -o -name ja -o -name ko -o -name pl -o -name pt-BR -o -name ru -o -name tr -o -name zh-Hans -o -name zh-Hant \) \ |
| 164 | -prune -exec rm -rf {} + \ |
| 165 | && chown -R node:node /usr/share/dotnet \ |
| 166 | && ln -s /usr/share/dotnet/dotnet /usr/local/bin/dotnet |
| 167 | ENV DOTNET_ROOT=/usr/share/dotnet \ |
| 168 | DOTNET_NOLOGO=1 \ |
| 169 | DOTNET_CLI_TELEMETRY_OPTOUT=1 \ |
| 170 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 \ |
| 171 | DOTNET_MULTILEVEL_LOOKUP=0 |
| 172 | |
| 173 | # Ruby 3.3, built from ruby-lang.org's source in the tool cache, where |
| 174 | # `ruby/setup-ruby` looks on a Debian machine (its prebuilt Rubies are for |
| 175 | # other systems), with the `x64.complete` marker it checks for. The prefix |
| 176 | # is fixed when Ruby is built, so it is built in place. One step, so the |
| 177 | # source and objects never reach a layer, and the previous base's inline |
| 178 | # cache covers it. |
| 179 | ARG RUBY_VERSION=3.3.12 |
| 180 | ARG RUBY_SHA256=b06d63beae271933033e27f0a389bc582a009e7845357d44365c39de525a051b |
| 181 | RUN prefix="/home/runner/_tool/Ruby/${RUBY_VERSION}/x64" \ |
| 182 | && curl -fsSLo /tmp/ruby.tgz "https://cache.ruby-lang.org/pub/ruby/${RUBY_VERSION%.*}/ruby-${RUBY_VERSION}.tar.gz" \ |
| 183 | && echo "${RUBY_SHA256} /tmp/ruby.tgz" | sha256sum -c - \ |
| 184 | && mkdir /tmp/ruby && tar -C /tmp/ruby --strip-components=1 -xzf /tmp/ruby.tgz \ |
| 185 | && cd /tmp/ruby \ |
| 186 | && ./configure --prefix="$prefix" --enable-shared --disable-install-doc \ |
| 187 | && make -j"$(nproc)" \ |
| 188 | && make install \ |
| 189 | && cd / && rm -rf /tmp/ruby /tmp/ruby.tgz \ |
| 190 | && "$prefix/bin/ruby" -ropenssl -rpsych -rzlib -rfiddle -e 'puts RUBY_DESCRIPTION' \ |
| 191 | && touch "$prefix.complete" \ |
| 192 | && chown -R node:node "/home/runner/_tool/Ruby" |
| 193 | ENV PATH=${JAVA_HOME}/bin:/home/runner/_tool/Ruby/${RUBY_VERSION}/x64/bin:$PATH |
| 194 | |
| 195 | USER node |
| 196 | |
| 197 | # Commits are the g1t agent's; the harness does not sign them as its own. |
| 198 | RUN mkdir -p /home/node/.claude \ |
| 199 | && echo '{"includeCoAuthoredBy": false}' > /home/node/.claude/settings.json |
| 200 | |
| 201 | # The Claude Code CLI, pinned, on top: it changes most often of anything |
| 202 | # here, and is one layer to rebuild and push. |
| 203 | ARG CLAUDE_CODE_VERSION=2.1.291 |
| 204 | USER root |
| 205 | # npm's cache stays in BuildKit's cache, out of the image. |
| 206 | RUN --mount=type=cache,target=/tmp/npm-cache \ |
| 207 | npm install --global --no-audit --no-fund --cache /tmp/npm-cache "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \ |
| 208 | && claude --version |
| 209 | USER node |