Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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