Skip to content
209 linesCodeBlameRaw
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
29FROM 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.
34RUN 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).
55RUN --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.
70ARG DOCKER_KEY_FINGERPRINT=9DC858229FC7DD38854AE2D88D81803C0EBFCD88
71RUN --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.
87RUN 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.
95ARG GO_VERSION=1.27.1
96ARG GO_SHA256=63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445
97RUN 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.
104USER node
105ENV HOME=/home/node
106ENV 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.
112RUN 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
119USER 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).
124RUN --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.
135ARG TEMURIN_RELEASE=jdk-21.0.12.1+1
136ARG TEMURIN_TOOLCACHE_VERSION=21.0.12-101.0.LTS
137ARG TEMURIN_SHA256=ce79869e1307ed8ee1e2baa86a412b1eb5b75d10a01006d788a6f968bcfaee94
138RUN 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
148ENV JAVA_HOME=/home/runner/_tool/Java_Temurin-Hotspot_jdk/${TEMURIN_TOOLCACHE_VERSION}/x64
149ENV 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.
155ARG DOTNET_SDK_VERSION=8.0.425
156ARG DOTNET_SDK_SHA512=934b8060a7190e5909ad1fd0785db542f487b3bbf6cdd14826b02095fdd0d0394298b1634085eff302928fccc33f7c1a7253e9b87df555fc36fce819bcd2e798
157RUN 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
167ENV 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.
179ARG RUBY_VERSION=3.3.12
180ARG RUBY_SHA256=b06d63beae271933033e27f0a389bc582a009e7845357d44365c39de525a051b
181RUN 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"
193ENV PATH=${JAVA_HOME}/bin:/home/runner/_tool/Ruby/${RUBY_VERSION}/x64/bin:$PATH
194
195USER node
196
197# Commits are the g1t agent's; the harness does not sign them as its own.
198RUN 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.
203ARG CLAUDE_CODE_VERSION=2.1.291
204USER root
205# npm's cache stays in BuildKit's cache, out of the image.
206RUN --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
209USER node