Add fat Ubuntu 26.04 base image with runit, Go, Node, Python toolchains
Single-RUN image built by build.sh on ubuntu:26.04: build-essential, distro plus latest Go, nvm with Node LTS, corepack with yarn/pnpm cached, wrangler, hugo extended, golangci-lint, pyenv with latest Python 3, pip and virtualenv. runit services supervised under runsvinit as PID 1. Docs, man pages, caches and non-English locales stripped. Includes build logs for the 2026-11-05 release (amd64 and arm64).
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
#
|
||||||
|
# Fat base image: current Ubuntu LTS with Go, Node, Python, Hugo, and the
|
||||||
|
# usual build tooling preinstalled, supervised by runit under runsvinit.
|
||||||
|
# Builds for linux/amd64 and linux/arm64 from the same definition.
|
||||||
|
# All the work happens in build.sh.
|
||||||
|
|
||||||
|
FROM ubuntu:26.04
|
||||||
|
|
||||||
|
# Pinned sources. Everything else (apt, Go, Node LTS, Python, Hugo,
|
||||||
|
# golangci-lint, yarn, wrangler) resolves to its current release at build.
|
||||||
|
ARG RUNSVINIT_COMMIT=b4b2c785308b1ce785b6155c7fe5f16879080193
|
||||||
|
ARG NVM_GIT_TAG=v0.41.0
|
||||||
|
ARG PYENV_GIT_TAG=v2.8.14
|
||||||
|
|
||||||
|
ENV LANG=C.UTF-8 \
|
||||||
|
NVM_DIR=/usr/local/nvm \
|
||||||
|
NVM_SYMLINK_CURRENT=true \
|
||||||
|
COREPACK_HOME=/usr/local/corepack \
|
||||||
|
COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||||
|
COREPACK_ENABLE_AUTO_PIN=0 \
|
||||||
|
PYENV_ROOT=/usr/local/pyenv \
|
||||||
|
GOPATH=/root/go \
|
||||||
|
PATH=/usr/local/go/bin:/root/go/bin:/usr/local/nvm/current/bin:/usr/local/pyenv/shims:/usr/local/pyenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
|
||||||
|
COPY rootfs/ /
|
||||||
|
ADD build.sh /tmp/build.sh
|
||||||
|
RUN bash /tmp/build.sh
|
||||||
|
|
||||||
|
CMD ["/usr/local/sbin/runsvinit"]
|
||||||
@@ -1,2 +1,192 @@
|
|||||||
# baseimage
|
# baseimage
|
||||||
|
|
||||||
|
A fat Docker base image that trades size for build speed. Everything a
|
||||||
|
typical Go, Node, Python, or Hugo project needs is preinstalled, so child
|
||||||
|
images only have to copy their source and build, instead of spending the
|
||||||
|
first few minutes of every build on `apt-get install` and toolchain
|
||||||
|
downloads.
|
||||||
|
|
||||||
|
Image: `git.eeqj.de/sneak/baseimage`, built for `linux/amd64` and
|
||||||
|
`linux/arm64`.
|
||||||
|
|
||||||
|
## What is in it
|
||||||
|
|
||||||
|
| Component | Source | Location |
|
||||||
|
| --------------- | ------------------------------ | ------------------------------ |
|
||||||
|
| Ubuntu 26.04 | `ubuntu:26.04`, fully upgraded | `/` |
|
||||||
|
| init | `runsvinit` (built from Go) | `/usr/local/sbin/runsvinit` |
|
||||||
|
| services | `runit` (apt) | `/etc/sv`, `/etc/service` |
|
||||||
|
| build-essential | apt | `gcc`, `make`, `pkg-config` |
|
||||||
|
| Go (distro) | apt `golang-go` | `/usr/lib/go-*` |
|
||||||
|
| Go (latest) | go.dev tarball | `/usr/local/go` |
|
||||||
|
| golangci-lint | GitHub release | `/usr/local/bin/golangci-lint` |
|
||||||
|
| Hugo (extended) | GitHub release `.deb` | `/usr/local/bin/hugo` |
|
||||||
|
| Node (distro) | apt `nodejs`, `npm` | `/usr/bin/node` |
|
||||||
|
| nvm | git tag | `/usr/local/nvm` |
|
||||||
|
| Node (LTS) | `nvm install --lts` | `/usr/local/nvm/current` |
|
||||||
|
| corepack | `npm install -g corepack` | nvm LTS global bin |
|
||||||
|
| yarn, pnpm | corepack, cache pre-populated | `/usr/local/corepack` |
|
||||||
|
| wrangler | `npm install -g wrangler` | nvm LTS global bin |
|
||||||
|
| Python (distro) | apt `python3`, `pip`, `venv` | `/usr/bin/python3` |
|
||||||
|
| pyenv | git tag | `/usr/local/pyenv` |
|
||||||
|
| Python (latest) | `pyenv install` | `/usr/local/pyenv/versions/*` |
|
||||||
|
| virtualenv | apt, and pip in pyenv Python | `virtualenv` on `PATH` |
|
||||||
|
|
||||||
|
Plus the usual odds and ends: `git`, `curl`, `wget`, `jq`, `rsync`,
|
||||||
|
`openssh-client`, `zip`/`unzip`, `xz-utils`, `bzip2`, and the `-dev`
|
||||||
|
headers needed to compile CPython and common native extensions.
|
||||||
|
|
||||||
|
The "latest" toolchains win on `PATH`. `go`, `node`, `npm`, `yarn`,
|
||||||
|
`python3`, and `pip` all resolve to the manually installed current
|
||||||
|
versions; the distro packages are there as a fallback and to satisfy apt
|
||||||
|
dependencies in child images.
|
||||||
|
|
||||||
|
`PATH` order:
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/local/go/bin
|
||||||
|
/root/go/bin
|
||||||
|
/usr/local/nvm/current/bin
|
||||||
|
/usr/local/pyenv/shims
|
||||||
|
/usr/local/pyenv/bin
|
||||||
|
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment variables set in the image: `NVM_DIR`, `NVM_SYMLINK_CURRENT`,
|
||||||
|
`COREPACK_HOME`, `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`,
|
||||||
|
`COREPACK_ENABLE_AUTO_PIN=0`, `PYENV_ROOT`, `GOPATH`, `LANG=C.UTF-8`.
|
||||||
|
|
||||||
|
### Yarn and corepack
|
||||||
|
|
||||||
|
`yarn` is provided by corepack, which is how the Yarn project ships Yarn
|
||||||
|
4. Corepack reads the `packageManager` field in a project's `package.json`
|
||||||
|
and runs exactly that version. The corepack cache in `/usr/local/corepack`
|
||||||
|
is pre-populated with current Yarn stable (the default when a project does
|
||||||
|
not pin one), Yarn 1 classic, and current pnpm, so projects pinning any of
|
||||||
|
those do not download anything at run time. A project pinning some other
|
||||||
|
version will be fetched on first use.
|
||||||
|
|
||||||
|
### Pinned versions
|
||||||
|
|
||||||
|
Only the sources that have no "latest" lookup are pinned, as build args in
|
||||||
|
the `Dockerfile`:
|
||||||
|
|
||||||
|
| Arg | What |
|
||||||
|
| ------------------ | -------------------------- |
|
||||||
|
| `RUNSVINIT_COMMIT` | `peterbourgon/runsvinit` |
|
||||||
|
| `NVM_GIT_TAG` | `nvm-sh/nvm` release tag |
|
||||||
|
| `PYENV_GIT_TAG` | `pyenv/pyenv` release tag |
|
||||||
|
|
||||||
|
Everything else resolves to its current release at build time: Ubuntu
|
||||||
|
packages, Go, Node LTS, Python, Hugo, golangci-lint, npm, yarn, pnpm,
|
||||||
|
wrangler.
|
||||||
|
|
||||||
|
## Services and init
|
||||||
|
|
||||||
|
PID 1 is [`runsvinit`](https://github.com/peterbourgon/runsvinit), a small
|
||||||
|
Go program written for exactly this job. It starts `runsvdir` on
|
||||||
|
`/etc/service`, reaps orphaned zombies, and on `SIGTERM` or `SIGINT` runs
|
||||||
|
`sv stop` on every service before letting `runsvdir` exit. `docker stop`
|
||||||
|
therefore stops services gracefully. The project is archived upstream, so
|
||||||
|
it is built from a pinned commit at image build time.
|
||||||
|
|
||||||
|
To add a service in a child image, create a runit service directory and
|
||||||
|
link it into `/etc/service`:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM git.eeqj.de/sneak/baseimage:latest
|
||||||
|
COPY sv/myapp/run /etc/sv/myapp/run
|
||||||
|
RUN chmod +x /etc/sv/myapp/run && ln -s /etc/sv/myapp /etc/service/myapp
|
||||||
|
```
|
||||||
|
|
||||||
|
A minimal `run` script:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
#!/bin/sh
|
||||||
|
exec 2>&1
|
||||||
|
exec /usr/local/bin/myapp
|
||||||
|
```
|
||||||
|
|
||||||
|
Services should log to stdout/stderr; the container's log is the combined
|
||||||
|
output of all services.
|
||||||
|
|
||||||
|
The image sets `CMD ["/usr/local/sbin/runsvinit"]` and no `ENTRYPOINT`, so
|
||||||
|
a child image that only needs one unsupervised process can override `CMD`
|
||||||
|
and ignore runit entirely, and `docker run -it image bash` works as
|
||||||
|
expected.
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
Installation leaves nothing behind that production does not need:
|
||||||
|
|
||||||
|
- apt lists and cache are removed, and `dpkg` is configured (see
|
||||||
|
`rootfs/etc/dpkg/dpkg.cfg.d/01_nodoc`) to exclude `/usr/share/doc`
|
||||||
|
(except copyright files), `/usr/share/man`, `/usr/share/info`, and
|
||||||
|
locales other than English. Package installs in child images inherit
|
||||||
|
that config.
|
||||||
|
- npm, nvm, pip, and Go build/module caches are emptied.
|
||||||
|
- Python test suites and the Go toolchain's test tree are stripped.
|
||||||
|
- `/root/go` is removed; it is recreated on first use.
|
||||||
|
|
||||||
|
Development headers (`-dev` packages) are deliberately kept: they are
|
||||||
|
what child images need to build things.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Native architecture only:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -t git.eeqj.de/sneak/baseimage:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release build
|
||||||
|
|
||||||
|
Each architecture is built natively on a host of that architecture (arm64
|
||||||
|
locally, amd64 on `fsn1app1` via `DOCKER_HOST=ssh://root@fsn1app1`), pushed
|
||||||
|
under a per-arch tag, and then joined into a multi-arch manifest:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# on each host, for its own arch
|
||||||
|
docker build --progress=plain -t baseimage:dev . 2>&1 | tee logs/YYYY-MM-DD/build-ARCH.log
|
||||||
|
docker tag baseimage:dev git.eeqj.de/sneak/baseimage:ARCH
|
||||||
|
docker push git.eeqj.de/sneak/baseimage:ARCH
|
||||||
|
|
||||||
|
# once both per-arch tags are in the registry
|
||||||
|
docker buildx imagetools create \
|
||||||
|
-t git.eeqj.de/sneak/baseimage:latest \
|
||||||
|
-t git.eeqj.de/sneak/baseimage:YYYY-MM-DD \
|
||||||
|
git.eeqj.de/sneak/baseimage:amd64 git.eeqj.de/sneak/baseimage:arm64
|
||||||
|
```
|
||||||
|
|
||||||
|
Tags:
|
||||||
|
|
||||||
|
| Tag | Meaning |
|
||||||
|
| -------------------- | ----------------------------------------- |
|
||||||
|
| `latest` | multi-arch manifest of the newest release |
|
||||||
|
| `YYYY-MM-DD` | multi-arch manifest of that day's release |
|
||||||
|
| `amd64`, `arm64` | per-arch images behind the newest release |
|
||||||
|
|
||||||
|
The full `--progress=plain` build log for each arch of each release is
|
||||||
|
committed under `logs/YYYY-MM-DD/`.
|
||||||
|
|
||||||
|
Every unpinned version is resolved at build time, so a rebuild picks up
|
||||||
|
current packages and tool releases. Rebuild and retag periodically; child
|
||||||
|
images get everything new on their next build.
|
||||||
|
|
||||||
|
The registry sits behind Traefik. The large `RUN` layer (about 650MB
|
||||||
|
compressed, over 2GB uncompressed) needs the entrypoint
|
||||||
|
`respondingTimeouts` raised from Traefik's 60 second default, or the final
|
||||||
|
blob commit fails with a 499 or 500 and docker re-uploads the layer.
|
||||||
|
|
||||||
|
Interactive shells (login shells, or anything that sources
|
||||||
|
`/etc/profile.d`) also get the `nvm` and `pyenv` shell functions from
|
||||||
|
`rootfs/etc/profile.d/baseimage.sh`.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `Dockerfile` — base, environment, one `COPY`, one `ADD`, one `RUN`.
|
||||||
|
- `build.sh` — everything the `RUN` does, in order: apt, Go, runsvinit,
|
||||||
|
golangci-lint, Hugo, nvm/Node/corepack/wrangler, pyenv/Python, cleanup,
|
||||||
|
sanity checks. Removes itself when done.
|
||||||
|
- `rootfs/` — files copied verbatim onto `/` before `build.sh` runs.
|
||||||
|
- `logs/YYYY-MM-DD/` — build logs for each released image, per arch.
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Image build script. Run once by the Dockerfile; removes itself when done.
|
||||||
|
# Everything that is not pinned by an environment variable resolves to its
|
||||||
|
# current release at build time.
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
arch="$(dpkg --print-architecture)"
|
||||||
|
|
||||||
|
latest_tag() {
|
||||||
|
curl -fsSLI -o /dev/null -w '%{url_effective}' \
|
||||||
|
"https://github.com/$1/releases/latest" | sed 's,.*/tag/,,'
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- distro packages -------------------------------------------------------
|
||||||
|
apt-get update
|
||||||
|
apt-get -y full-upgrade
|
||||||
|
apt-get -y install --no-install-recommends \
|
||||||
|
bzip2 ca-certificates curl git gnupg jq less openssh-client rsync \
|
||||||
|
unzip wget xz-utils zip \
|
||||||
|
build-essential make pkg-config \
|
||||||
|
runit \
|
||||||
|
golang-go \
|
||||||
|
nodejs npm \
|
||||||
|
python3 python3-dev python3-pip python3-venv virtualenv \
|
||||||
|
libbz2-dev libffi-dev libgdbm-dev liblzma-dev libncurses-dev \
|
||||||
|
libreadline-dev libsqlite3-dev libssl-dev libxml2-dev libxmlsec1-dev \
|
||||||
|
tk-dev uuid-dev zlib1g-dev
|
||||||
|
|
||||||
|
# --- Go (latest) -------------------------------------------------------------
|
||||||
|
gover="$(curl -fsSL 'https://go.dev/VERSION?m=text' | head -n1)"
|
||||||
|
curl -fsSL "https://go.dev/dl/${gover}.linux-${arch}.tar.gz" | tar -C /usr/local -xz
|
||||||
|
rm -rf /usr/local/go/test
|
||||||
|
|
||||||
|
# --- runsvinit (PID 1: reaps zombies, stops services on SIGTERM) -------------
|
||||||
|
git clone -q https://github.com/peterbourgon/runsvinit /tmp/runsvinit
|
||||||
|
git -C /tmp/runsvinit checkout -q "$RUNSVINIT_COMMIT"
|
||||||
|
(
|
||||||
|
cd /tmp/runsvinit
|
||||||
|
go mod init runsvinit >/dev/null 2>&1
|
||||||
|
CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /usr/local/sbin/runsvinit .
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- golangci-lint -----------------------------------------------------------
|
||||||
|
gl="$(latest_tag golangci/golangci-lint)"
|
||||||
|
curl -fsSL "https://github.com/golangci/golangci-lint/releases/download/${gl}/golangci-lint-${gl#v}-linux-${arch}.tar.gz" |
|
||||||
|
tar -C /usr/local/bin -xz --strip-components=1 "golangci-lint-${gl#v}-linux-${arch}/golangci-lint"
|
||||||
|
|
||||||
|
# --- Hugo (extended) ---------------------------------------------------------
|
||||||
|
hv="$(latest_tag gohugoio/hugo)"
|
||||||
|
curl -fsSL -o /tmp/hugo.deb \
|
||||||
|
"https://github.com/gohugoio/hugo/releases/download/${hv}/hugo_extended_${hv#v}_linux-${arch}.deb"
|
||||||
|
apt-get -y install /tmp/hugo.deb
|
||||||
|
|
||||||
|
# --- nvm, Node LTS, yarn, wrangler ------------------------------------------
|
||||||
|
git clone -q --depth 1 --branch "$NVM_GIT_TAG" https://github.com/nvm-sh/nvm.git "$NVM_DIR"
|
||||||
|
rm -rf "$NVM_DIR/.git"
|
||||||
|
set +ux # nvm.sh is not clean under set -u / too noisy under set -x
|
||||||
|
. "$NVM_DIR/nvm.sh"
|
||||||
|
nvm install --lts
|
||||||
|
nvm alias default "lts/*"
|
||||||
|
nvm use default
|
||||||
|
set -ux
|
||||||
|
npm install -g npm@latest corepack wrangler
|
||||||
|
corepack enable
|
||||||
|
# Pre-populate the corepack cache so projects that pin a package manager
|
||||||
|
# do not download at run time. The last yarn installed becomes the default.
|
||||||
|
corepack install -g pnpm@latest
|
||||||
|
corepack install -g yarn@1
|
||||||
|
corepack install -g yarn@stable
|
||||||
|
# pnpm 10+ fetches a standalone binary on first run; trigger that now so
|
||||||
|
# it lands in COREPACK_HOME.
|
||||||
|
pnpm --version
|
||||||
|
yarn --version
|
||||||
|
nvm cache clear
|
||||||
|
|
||||||
|
# --- pyenv, Python (latest), pip, virtualenv ---------------------------------
|
||||||
|
git clone -q --depth 1 --branch "$PYENV_GIT_TAG" https://github.com/pyenv/pyenv.git "$PYENV_ROOT"
|
||||||
|
rm -rf "$PYENV_ROOT/.git"
|
||||||
|
pyver="$(pyenv latest --known 3)"
|
||||||
|
pyenv install "$pyver"
|
||||||
|
pyenv global "$pyver"
|
||||||
|
rm -rf "$PYENV_ROOT"/versions/*/lib/python3*/test
|
||||||
|
python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel virtualenv
|
||||||
|
pyenv rehash
|
||||||
|
|
||||||
|
# --- cleanup -----------------------------------------------------------------
|
||||||
|
go clean -cache -modcache -testcache
|
||||||
|
apt-get -y autoremove --purge
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/* /var/cache/apt/* /var/cache/debconf/*-old /var/log/*
|
||||||
|
rm -rf /usr/share/doc /usr/share/man /usr/share/info /usr/share/lintian
|
||||||
|
find /usr/share/locale -mindepth 1 -maxdepth 1 ! -name 'en*' ! -name 'locale.alias' -exec rm -rf {} +
|
||||||
|
rm -rf /root/.cache /root/.npm /root/.config /root/go /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
# --- sanity ------------------------------------------------------------------
|
||||||
|
go version
|
||||||
|
golangci-lint version
|
||||||
|
hugo version
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
COREPACK_ENABLE_NETWORK=0 yarn --version
|
||||||
|
COREPACK_ENABLE_NETWORK=0 pnpm --version
|
||||||
|
wrangler --version
|
||||||
|
python3 --version
|
||||||
|
pip --version
|
||||||
|
virtualenv --version
|
||||||
|
test -x /usr/local/sbin/runsvinit
|
||||||
|
test -d /etc/service
|
||||||
|
|
||||||
|
# The checks above leave caches behind (node compile cache etc).
|
||||||
|
rm -rf /tmp/* /var/tmp/* /root/.cache /root/.npm /root/.config
|
||||||
|
rm -f "$0"
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
|||||||
|
# Keep documentation and non-English locales out of the image. Applies to
|
||||||
|
# every package installed here and in child images.
|
||||||
|
path-exclude /usr/share/doc/*
|
||||||
|
path-include /usr/share/doc/*/copyright
|
||||||
|
path-exclude /usr/share/man/*
|
||||||
|
path-exclude /usr/share/info/*
|
||||||
|
path-exclude /usr/share/lintian/*
|
||||||
|
path-exclude /usr/share/locale/*
|
||||||
|
path-include /usr/share/locale/en*
|
||||||
|
path-include /usr/share/locale/locale.alias
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Interactive shells get the nvm and pyenv shell functions. Non-interactive
|
||||||
|
# use does not need this: the image's PATH already points at the installed
|
||||||
|
# toolchains.
|
||||||
|
export NVM_DIR="${NVM_DIR:-/usr/local/nvm}"
|
||||||
|
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" --no-use
|
||||||
|
|
||||||
|
export PYENV_ROOT="${PYENV_ROOT:-/usr/local/pyenv}"
|
||||||
|
command -v pyenv >/dev/null 2>&1 && eval "$(pyenv init - --no-rehash)"
|
||||||
Reference in New Issue
Block a user