Files
kandev/Dockerfile.universal
2026-06-28 20:35:59 +01:00

221 lines
10 KiB
Docker

# Kandev Universal Dockerfile
#
# Extends the vanilla kandev image with language toolchains and developer
# tools so agents can build/test code in the most common stacks without
# extra setup. Built and published alongside vanilla in the release
# workflow. Inclusion policy: see docs/images.md.
#
# Build:
# docker build -f Dockerfile.universal -t kandev:latest-universal .
#
# Run (drop-in replacement for the vanilla image):
# docker run -p 38429:38429 -v kandev-data:/data ghcr.io/kdlbs/kandev:universal
#
# The BASE_IMAGE arg lets the release workflow point at a freshly built
# vanilla image inside the same run (so universal-of-X.Y.Z is built on top
# of vanilla-X.Y.Z, not on whatever :latest happens to be).
ARG BASE_IMAGE=ghcr.io/kdlbs/kandev:latest
FROM ${BASE_IMAGE}
USER root
# Pinned versions. Bump these deliberately; CI's size gate will catch growth.
#
# The Playwright Chromium apt deps below are hand-derived against Playwright
# 1.48.0. When that target moves, the apt-get install line may need to
# add/drop packages - check the upstream list at
# https://github.com/microsoft/playwright/blob/v<version>/packages/playwright-core/src/server/registry/nativeDeps.ts
# or run `npx playwright@<version> install-deps --dry-run` to diff. (Tracked
# only via this comment, not an ARG, since the apt package names are hardcoded
# below and a build-arg override would silently disagree with what's installed.)
ARG GO_VERSION=1.26.3
ARG GOLANGCI_LINT_VERSION=1.62.0
ARG PNPM_VERSION=9.15.9
# Single RUN to minimize image layers and clean apt lists in the same layer.
# Tools grouped by purpose; comments explain the why for each cluster so
# future maintainers can prune deliberately rather than guess.
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
# Tarball fetcher for the toolchain install steps below (Go,
# golangci-lint, rustup, yq). Not provided by the vanilla base image.
curl \
# Native compile (CGO, pip wheels with C extensions, native npm modules)
build-essential \
pkg-config \
python3-dev \
libssl-dev \
# Useful CLI tools agents reach for. Small, broadly applicable.
ripgrep \
fd-find \
jq \
# Playwright Chromium runtime deps. Browsers themselves are NOT
# preinstalled - users run `pnpm exec playwright install chromium`
# once and the download persists at ~/.cache/ms-playwright on the PV.
# This list mirrors `npx playwright@1.48.0 install-deps chromium`.
libnss3 \
libnspr4 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libcups2 \
libdrm2 \
libxkbcommon0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libpango-1.0-0 \
libcairo2 \
libasound2 \
fonts-liberation \
fonts-noto-color-emoji \
; \
rm -rf /var/lib/apt/lists/*
# Go toolchain - tarball install to /usr/local/go (the standard layout the
# go.dev installer creates). Add to PATH below.
#
# Integrity: go.dev's `?mode=json&include=all` endpoint lists every released
# version with a per-file `sha256` field. We fetch that index, pick the SHA
# for our tarball, and verify before extraction. (The `.sha256` sidecar URLs
# that look like they should work redirect to the HTML downloads page, so
# they cannot be used here.) This is TOFU on first fetch; if go.dev itself
# is compromised both the JSON and the tarball are bad.
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
amd64) goarch=amd64 ;; \
arm64) goarch=arm64 ;; \
*) echo "unsupported arch: $arch" >&2; exit 1 ;; \
esac; \
tarball="go${GO_VERSION}.linux-${goarch}.tar.gz"; \
expected="$(curl -fsSL 'https://go.dev/dl/?mode=json&include=all' \
| jq -r --arg v "go${GO_VERSION}" --arg f "${tarball}" \
'first(.[] | select(.version == $v) | .files[] | select(.filename == $f) | .sha256)')"; \
if [ -z "$expected" ]; then \
echo "no upstream sha256 for ${tarball} at version ${GO_VERSION}" >&2; \
exit 1; \
fi; \
curl -fsSL "https://go.dev/dl/${tarball}" -o /tmp/go.tar.gz; \
echo "${expected} /tmp/go.tar.gz" | sha256sum -c -; \
tar -C /usr/local -xzf /tmp/go.tar.gz; \
rm /tmp/go.tar.gz
# golangci-lint - prebuilt binary, no compilation needed.
# Integrity: each release publishes a single `*-checksums.txt` file with
# `<sha256> <filename>` lines for every artifact. Grep for our tarball,
# pipe to `sha256sum -c -`. Download into /tmp and cd there so the relative
# filename in the checksum line resolves.
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
amd64) lintarch=amd64 ;; \
arm64) lintarch=arm64 ;; \
*) echo "unsupported arch: $arch" >&2; exit 1 ;; \
esac; \
tarball="golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${lintarch}.tar.gz"; \
base="https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCI_LINT_VERSION}"; \
curl -fsSL "${base}/${tarball}" -o "/tmp/${tarball}"; \
curl -fsSL "${base}/golangci-lint-${GOLANGCI_LINT_VERSION}-checksums.txt" -o /tmp/golangci-lint-checksums.txt; \
(cd /tmp && grep " ${tarball}$" golangci-lint-checksums.txt | sha256sum -c -); \
tar -xz -C /tmp -f "/tmp/${tarball}"; \
mv "/tmp/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${lintarch}/golangci-lint" /usr/local/bin/; \
rm -rf "/tmp/${tarball}" "/tmp/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${lintarch}" /tmp/golangci-lint-checksums.txt
# Rust toolchain via rustup. RUSTUP_HOME/CARGO_HOME live in /usr/local so the
# install is system-wide and read-only for non-root users at runtime -
# matching the rest of the image. Users who need writable per-user state
# (e.g. `cargo install <tool>`, `rustup toolchain install nightly`) should
# override CARGO_HOME and RUSTUP_HOME to a path on the PV, e.g.:
# CARGO_HOME=~/.cargo RUSTUP_HOME=~/.rustup cargo install <tool>
#
# `--profile minimal` keeps the install lean (rustc + cargo + rust-std); we
# then explicitly add clippy and rustfmt, which agents working on Rust
# projects almost always reach for. `--profile default` would also work but
# pulls in rust-docs which we don't need.
#
# Integrity: download rustup-init for our arch and verify its SHA-256
# against the upstream .sha256 sidecar before executing - matches the
# pattern used for golangci-lint/yq above, rather than `curl | sh`.
# (Go uses the JSON release index instead - go.dev doesn't actually
# publish per-tarball .sha256 sidecars despite the URL pattern looking
# like it would work.)
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
amd64) triplet=x86_64-unknown-linux-gnu ;; \
arm64) triplet=aarch64-unknown-linux-gnu ;; \
*) echo "unsupported arch: $arch" >&2; exit 1 ;; \
esac; \
base="https://static.rust-lang.org/rustup/dist/${triplet}"; \
curl -fsSL "${base}/rustup-init" -o /tmp/rustup-init; \
curl -fsSL "${base}/rustup-init.sha256" -o /tmp/rustup-init.sha256; \
(cd /tmp && sha256sum -c rustup-init.sha256); \
chmod +x /tmp/rustup-init; \
/tmp/rustup-init -y --default-toolchain stable --no-modify-path --profile minimal; \
/usr/local/cargo/bin/rustup component add --toolchain stable clippy rustfmt; \
rm /tmp/rustup-init /tmp/rustup-init.sha256; \
chmod -R a+rX "$RUSTUP_HOME" "$CARGO_HOME"
# pnpm via npm - install system-wide so users don't need to bootstrap it
# themselves. Drops shims into /usr/local/bin.
#
# No separate sha256 step: npm downloads from registry.npmjs.org and verifies
# the tarball against the registry's `integrity` field (SHA-512) before
# installing. The other tools above pin upstream `.sha256` sidecars because
# they're served from CDNs that don't expose package integrity at the API level;
# npm does. Decision is deliberate, not an oversight.
RUN set -eux; \
npm install -g --prefix /usr/local "pnpm@${PNPM_VERSION}"; \
pnpm --version
# fd is installed as `fdfind` on Debian to avoid clashing with the legacy
# fd package - symlink so agents calling plain `fd` still work.
RUN ln -s /usr/bin/fdfind /usr/local/bin/fd
# yq is not in Debian stable; install the prebuilt binary from upstream
# (Mike Farah's yq, the Go implementation - the one most scripts assume).
#
# Integrity: mikefarah/yq publishes a `checksums` file with one row per
# artifact and multiple hash columns, plus a `checksums_hashes_order` file
# that lists which hash algorithm corresponds to each column. We compute the
# SHA-256 column index dynamically (defensive against upstream reordering)
# and verify the binary against that column before installing.
ARG YQ_VERSION=4.44.3
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
target="yq_linux_${arch}"; \
base="https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}"; \
curl -fsSL "${base}/checksums" -o /tmp/yq-checksums; \
curl -fsSL "${base}/checksums_hashes_order" -o /tmp/yq-hashes-order; \
sha256_col="$(awk '/^SHA-256$/ {print NR + 1; exit}' /tmp/yq-hashes-order)"; \
if [ -z "$sha256_col" ]; then echo "SHA-256 not in upstream checksums_hashes_order" >&2; exit 1; fi; \
expected="$(awk -v t="$target" -v c="$sha256_col" '$1==t {print $c}' /tmp/yq-checksums)"; \
if [ -z "$expected" ]; then echo "no checksum row for $target" >&2; exit 1; fi; \
curl -fsSL "${base}/${target}" -o /tmp/yq; \
echo "${expected} /tmp/yq" | sha256sum -c -; \
mv /tmp/yq /usr/local/bin/yq; \
chmod +x /usr/local/bin/yq; \
rm /tmp/yq-checksums /tmp/yq-hashes-order
# Append Go's toolchain bin and cargo's bin to whatever PATH the base image
# set. Appending rather than hardcoding the full list keeps the agent CLI
# dir (/data/.npm-global/bin) first - as the base image already arranges -
# and stays correct if the base image's PATH ever changes.
#
# Note: $GOPATH/bin (defaults to ~/go/bin, i.e. /data/home/go/bin in this
# image) is NOT added here - it's user-specific and lives on the PV. Users
# who `go install` tools and want them on PATH should add it to their shell
# profile (e.g. `export PATH=$PATH:$(go env GOPATH)/bin`).
ENV PATH=$PATH:/usr/local/go/bin:/usr/local/cargo/bin
# Drop back to the unprivileged user. Matches the vanilla image's runtime
# user; the docker-entrypoint.sh in the base image handles root → kandev
# privilege drop via gosu for users who run the container as root.
USER kandev