Files
kandev/Dockerfile.universal
T

207 lines
9.7 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.0
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 \
# 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 publishes a per-tarball `.sha256` file (a bare hex hash,
# no filename), fetched alongside the tarball and verified before extraction.
# This is TOFU on first fetch; if the go.dev CDN itself is compromised both
# files are bad. Strongest available option without bumping the hash inline
# every Go release.
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
amd64) goarch=amd64 ;; \
arm64) goarch=arm64 ;; \
*) echo "unsupported arch: $arch" >&2; exit 1 ;; \
esac; \
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${goarch}.tar.gz" -o /tmp/go.tar.gz; \
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${goarch}.tar.gz.sha256" -o /tmp/go.tar.gz.sha256; \
echo "$(cat /tmp/go.tar.gz.sha256) /tmp/go.tar.gz" | sha256sum -c -; \
tar -C /usr/local -xzf /tmp/go.tar.gz; \
rm /tmp/go.tar.gz /tmp/go.tar.gz.sha256
# 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 Go/golangci-lint/yq above, rather than `curl | sh`.
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 corepack - install system-wide so users don't need
# `corepack enable` themselves. Drops shims into /usr/local/bin.
#
# No separate sha256 step: corepack downloads from registry.npmjs.org and
# verifies the tarball against the registry's `integrity` field (SHA-512)
# before activating. 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; \
corepack enable; \
corepack prepare "pnpm@${PNPM_VERSION}" --activate
# 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