221 lines
10 KiB
Docker
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 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
|