Compare commits

4 Commits
Author SHA1 Message Date
sneak 593e623cff The base image never starts a service (closes #16)
build.sh and bin/test used to start valkey-server and ping it to prove it runs;
they now check it with valkey-server --version and valkey-cli --version, so the
ping loop, shutdown, and the cleanup of the Valkey run's log files are gone.
/var/log/valkey and /var/log/nginx keep their package owners and modes, and
nginx -t stays. README states that Valkey, PostgreSQL and nginx are installed
but never run. No package install starts a service: invoke-rc.d already denies
every start, and no ENTRYPOINT or CMD starts one.

Model: deepseek-v4-pro
2026-11-05 22:53:40 +00:00
sneak 8f976e38e0 Add nginx (closes #11)
Install nginx from apt and check nginx -t in the smoke test.

Model: deepseek-v4-pro
2026-11-05 15:45:26 +00:00
sneak 574c5cdbce Add browsers for tests: Playwright, Firefox, geckodriver, headless Chrome (closes #10)
Firefox comes from Mozilla's own apt repo, pinned above Ubuntu's snap-only
package. geckodriver is at its latest release, independent of the Firefox
version. Playwright installs its Chromium and Firefox; WebKit is skipped
because no repository uses it today. Playwright's Chromium is exposed as
the headless Chrome.

Model: deepseek-v4-pro
2026-11-05 15:45:26 +00:00
sneak 69fc40651e Add Valkey and PostgreSQL (closes #9)
Install valkey-server, valkey-tools and postgresql from apt. Ubuntu
26.04 ships Valkey 9 and PostgreSQL 18, replacing the postgresql16 the
audit pinned.

Model: deepseek-v4-pro
2026-11-05 15:45:26 +00:00
4 changed files with 136 additions and 47 deletions
+1
View File
@@ -20,6 +20,7 @@ ENV LANG=C.UTF-8 \
COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
COREPACK_ENABLE_AUTO_PIN=0 \ COREPACK_ENABLE_AUTO_PIN=0 \
PYENV_ROOT=/usr/local/pyenv \ PYENV_ROOT=/usr/local/pyenv \
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
GOPATH=/root/go \ 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 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
+33 -13
View File
@@ -1,10 +1,9 @@
# baseimage # baseimage
A fat Docker base image that trades size for build speed. Everything a 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 typical Go, Node, Python, or Hugo project needs is preinstalled. Child
images only have to copy their source and build, instead of spending the images only copy their source and build, without spending the first
first few minutes of every build on `apt-get install` and toolchain minutes of every build on `apt-get install` and toolchain downloads.
downloads.
Image: `git.eeqj.de/sneak/baseimage`, built for `linux/amd64` and Image: `git.eeqj.de/sneak/baseimage`, built for `linux/amd64` and
`linux/arm64`. `linux/arm64`.
@@ -12,7 +11,7 @@ Image: `git.eeqj.de/sneak/baseimage`, built for `linux/amd64` and
## What is in it ## What is in it
| Component | Source | Location | | Component | Source | Location |
| --------------- | ------------------------------ | ------------------------------ | | ------------------- | ------------------------------------------ | ------------------------------------------------ |
| Ubuntu 26.04 | `ubuntu:26.04`, fully upgraded | `/` | | Ubuntu 26.04 | `ubuntu:26.04`, fully upgraded | `/` |
| init | `runsvinit` (built from Go) | `/usr/local/sbin/runsvinit` | | init | `runsvinit` (built from Go) | `/usr/local/sbin/runsvinit` |
| services | `runit` (apt) | `/etc/sv`, `/etc/service` | | services | `runit` (apt) | `/etc/sv`, `/etc/service` |
@@ -30,6 +29,11 @@ Image: `git.eeqj.de/sneak/baseimage`, built for `linux/amd64` and
| yarn, pnpm | corepack, cache pre-populated | `/usr/local/corepack` | | yarn, pnpm | corepack, cache pre-populated | `/usr/local/corepack` |
| wrangler | `npm install -g wrangler` | nvm LTS global bin | | wrangler | `npm install -g wrangler` | nvm LTS global bin |
| prettier | `npm install -g prettier` | nvm LTS global bin | | prettier | `npm install -g prettier` | nvm LTS global bin |
| Playwright | `npm install -g playwright` | nvm LTS global bin |
| Playwright browsers | `playwright install` (chromium, firefox) | `/ms-playwright` |
| Firefox | Mozilla apt repo | `/usr/bin/firefox` |
| geckodriver | GitHub release | `/usr/local/bin/geckodriver` |
| headless Chrome | Playwright Chromium | `chromium` on `PATH` |
| Python (distro) | apt `python3`, `pip`, `venv` | `/usr/bin/python3` | | Python (distro) | apt `python3`, `pip`, `venv` | `/usr/bin/python3` |
| pyenv | git tag | `/usr/local/pyenv` | | pyenv | git tag | `/usr/local/pyenv` |
| Python (latest) | `pyenv install` | `/usr/local/pyenv/versions/*` | | Python (latest) | `pyenv install` | `/usr/local/pyenv/versions/*` |
@@ -43,6 +47,9 @@ Image: `git.eeqj.de/sneak/baseimage`, built for `linux/amd64` and
| Ghostscript | apt | `gs` | | Ghostscript | apt | `gs` |
| Ruby | apt `ruby`, `ruby-dev` | `/usr/bin/ruby` | | Ruby | apt `ruby`, `ruby-dev` | `/usr/bin/ruby` |
| Bundler | apt `bundler` | `bundle` on `PATH` | | Bundler | apt `bundler` | `bundle` on `PATH` |
| Valkey | apt `valkey-server`, `valkey-tools` | `valkey-server`, `valkey-cli` |
| PostgreSQL | apt `postgresql` | `psql`, `postgres`, `initdb`, `pg_ctl` on `PATH` |
| nginx | apt | `/usr/sbin/nginx` |
Plus the usual odds and ends: `git`, `curl`, `wget`, `jq`, `rsync`, Plus the usual odds and ends: `git`, `curl`, `wget`, `jq`, `rsync`,
`openssh-client`, `zip`/`unzip`, `xz-utils`, `bzip2`, and the `-dev` `openssh-client`, `zip`/`unzip`, `xz-utils`, `bzip2`, and the `-dev`
@@ -66,7 +73,8 @@ dependencies in child images.
Environment variables set in the image: `NVM_DIR`, `NVM_SYMLINK_CURRENT`, Environment variables set in the image: `NVM_DIR`, `NVM_SYMLINK_CURRENT`,
`COREPACK_HOME`, `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`, `COREPACK_HOME`, `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`,
`COREPACK_ENABLE_AUTO_PIN=0`, `PYENV_ROOT`, `GOPATH`, `LANG=C.UTF-8`. `COREPACK_ENABLE_AUTO_PIN=0`, `PYENV_ROOT`, `GOPATH`, `LANG=C.UTF-8`,
`PLAYWRIGHT_BROWSERS_PATH`.
### Yarn and corepack ### Yarn and corepack
@@ -89,9 +97,18 @@ the `Dockerfile`:
| `NVM_GIT_TAG` | `nvm-sh/nvm` release tag | | `NVM_GIT_TAG` | `nvm-sh/nvm` release tag |
| `PYENV_GIT_TAG` | `pyenv/pyenv` release tag | | `PYENV_GIT_TAG` | `pyenv/pyenv` release tag |
Everything else resolves to its current release at build time: Ubuntu Everything else resolves to its current release at build time:
packages, Go, Node LTS, Python, Hugo, golangci-lint, npm, yarn, pnpm,
wrangler. - Ubuntu packages
- Go, Node LTS, and Python
- Hugo and golangci-lint
- npm, yarn, pnpm, wrangler, and prettier
- Playwright and its browsers
- Firefox and geckodriver
Playwright installs its browsers for the image's Playwright version only. A
child image that pins a different Playwright version downloads its own
browsers.
## Services and init ## Services and init
@@ -122,6 +139,9 @@ exec /usr/local/bin/myapp
Services should log to stdout/stderr; the container's log is the combined Services should log to stdout/stderr; the container's log is the combined
output of all services. output of all services.
The image installs Valkey, PostgreSQL and nginx but never runs them; a child
image that wants one runs it as a service.
The image sets `CMD ["/usr/local/sbin/runsvinit"]` and no `ENTRYPOINT`, so The image sets `CMD ["/usr/local/sbin/runsvinit"]` and no `ENTRYPOINT`, so
a child image that only needs one unsupervised process can override `CMD` a child image that only needs one unsupervised process can override `CMD`
and ignore runit entirely, and `docker run -it image bash` works as and ignore runit entirely, and `docker run -it image bash` works as
@@ -184,8 +204,8 @@ Every unpinned version is resolved at build time, so a rebuild picks up
current packages and tool releases. Rebuild and retag periodically; child current packages and tool releases. Rebuild and retag periodically; child
images get everything new on their next build. images get everything new on their next build.
The registry sits behind Traefik. The large `RUN` layer (about 650MB The registry sits behind Traefik. The large `RUN` layer (about 1.63 GB
compressed, over 2GB uncompressed) needs the entrypoint compressed, 4.3 GB on disk) needs the entrypoint
`respondingTimeouts` raised from Traefik's 60 second default, or the final `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. blob commit fails with a 499 or 500 and docker re-uploads the layer.
@@ -200,7 +220,7 @@ Interactive shells (login shells, or anything that sources
`commit-logs`, `clean`, plus `lib.sh` (shared setup) and `native-arch`. `commit-logs`, `clean`, plus `lib.sh` (shared setup) and `native-arch`.
- `Dockerfile` — base, environment, one `COPY`, one `ADD`, one `RUN`. - `Dockerfile` — base, environment, one `COPY`, one `ADD`, one `RUN`.
- `build.sh` — everything the `RUN` does, in order: apt, Go, runsvinit, - `build.sh` — everything the `RUN` does, in order: apt, Go, runsvinit,
golangci-lint, Hugo, nvm/Node/corepack/wrangler, pyenv/Python, cleanup, golangci-lint, govulncheck, Hugo, nvm/Node/corepack/wrangler, browsers,
sanity checks. Removes itself when done. pyenv/Python, cleanup, sanity checks. Removes itself when done.
- `rootfs/` — files copied verbatim onto `/` before `build.sh` runs. - `rootfs/` — files copied verbatim onto `/` before `build.sh` runs.
- `logs/YYYY-MM-DD/` — build logs for each released image, per arch. - `logs/YYYY-MM-DD/` — build logs for each released image, per arch.
+9
View File
@@ -32,6 +32,15 @@ docker run --rm "$img" bash -ec '
gs --version gs --version
ruby --version ruby --version
bundle --version bundle --version
valkey-server --version
valkey-cli --version
psql --version
postgres --version
nginx -t
playwright --version
firefox --version
geckodriver --version
chromium --version
' '
echo "== $img: runit service lifecycle" echo "== $img: runit service lifecycle"
+62 -3
View File
@@ -31,7 +31,15 @@ apt-get -y install --no-install-recommends \
zstd sqlite3 \ zstd sqlite3 \
libvips-dev libvips-tools libheif-dev libheif-plugin-aomenc \ libvips-dev libvips-tools libheif-dev libheif-plugin-aomenc \
imagemagick ghostscript \ imagemagick ghostscript \
ruby ruby-dev bundler ruby ruby-dev bundler \
valkey-server valkey-tools postgresql \
nginx
# Link the PostgreSQL server binaries onto PATH. They live under a versioned
# directory (/usr/lib/postgresql/*/bin); the client psql is already on PATH.
for p in postgres initdb pg_ctl; do
ln -s /usr/lib/postgresql/*/bin/"$p" /usr/local/bin/"$p"
done
# --- Go (latest) ------------------------------------------------------------- # --- Go (latest) -------------------------------------------------------------
gover="$(curl -fsSL 'https://go.dev/VERSION?m=text' | head -n1)" gover="$(curl -fsSL 'https://go.dev/VERSION?m=text' | head -n1)"
@@ -70,7 +78,7 @@ nvm install --lts
nvm alias default "lts/*" nvm alias default "lts/*"
nvm use default nvm use default
set -ux set -ux
npm install -g npm@latest corepack wrangler prettier npm install -g npm@latest corepack wrangler prettier playwright
corepack enable corepack enable
# Pre-populate the corepack cache so projects that pin a package manager # 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. # do not download at run time. The last yarn installed becomes the default.
@@ -83,6 +91,42 @@ pnpm --version
yarn --version yarn --version
nvm cache clear nvm cache clear
# --- browsers for tests: Firefox, geckodriver, Playwright --------------------
# Ubuntu's firefox is a snap that does not run in a container, so use
# Mozilla's own apt repository for the real binary.
install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://packages.mozilla.org/apt/repo-signing-key.gpg \
-o /etc/apt/keyrings/packages.mozilla.org.asc
echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" \
> /etc/apt/sources.list.d/mozilla.list
cat > /etc/apt/preferences.d/mozilla <<'EOF'
Package: *
Pin: origin packages.mozilla.org
Pin-Priority: 1000
EOF
apt-get update
apt-get -y install --no-install-recommends -t mozilla firefox
# geckodriver, at its latest release (independent of the Firefox version).
gd="$(latest_tag mozilla/geckodriver)"
case "$arch" in
arm64) gd_arch=linux-aarch64 ;;
*) gd_arch=linux64 ;;
esac
curl -fsSL "https://github.com/mozilla/geckodriver/releases/download/${gd}/geckodriver-${gd}-${gd_arch}.tar.gz" |
tar -C /usr/local/bin -xz geckodriver
# Playwright's Chromium and Firefox. WebKit is skipped because no repository
# uses it today. Playwright's Chromium is also the headless Chrome for
# chromedp-style clients, exposed as `chromium` on PATH.
playwright install --with-deps chromium firefox
chromiums=( "$PLAYWRIGHT_BROWSERS_PATH"/chromium-*/chrome-linux*/chrome )
if [ "${#chromiums[@]}" -ne 1 ] || [ ! -x "${chromiums[0]}" ]; then
echo "error: expected exactly one chromium binary, found ${#chromiums[@]}" >&2
exit 1
fi
ln -s "${chromiums[0]}" /usr/local/bin/chromium
# --- pyenv, Python (latest), pip, virtualenv --------------------------------- # --- pyenv, Python (latest), pip, virtualenv ---------------------------------
git clone -q --depth 1 --branch "$PYENV_GIT_TAG" https://github.com/pyenv/pyenv.git "$PYENV_ROOT" git clone -q --depth 1 --branch "$PYENV_GIT_TAG" https://github.com/pyenv/pyenv.git "$PYENV_ROOT"
rm -rf "$PYENV_ROOT/.git" rm -rf "$PYENV_ROOT/.git"
@@ -102,6 +146,11 @@ 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 {} + 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/* rm -rf /root/.cache /root/.npm /root/.config /root/go /tmp/* /var/tmp/*
# Recreate the log directories the cleanup above removed, with their package
# owners, so child images have them ready without recreating them at startup.
install -d -o root -g adm /var/log/nginx
install -d -o valkey -g adm -m 2750 /var/log/valkey
# --- sanity ------------------------------------------------------------------ # --- sanity ------------------------------------------------------------------
go version go version
govulncheck -version govulncheck -version
@@ -116,6 +165,11 @@ magick --version
gs --version gs --version
ruby --version ruby --version
bundle --version bundle --version
valkey-server --version
valkey-cli --version
psql --version
postgres --version
nginx -t
hugo version hugo version
node --version node --version
npm --version npm --version
@@ -123,12 +177,17 @@ COREPACK_ENABLE_NETWORK=0 yarn --version
COREPACK_ENABLE_NETWORK=0 pnpm --version COREPACK_ENABLE_NETWORK=0 pnpm --version
wrangler --version wrangler --version
prettier --version prettier --version
playwright --version
firefox --version
geckodriver --version
chromium --version
python3 --version python3 --version
pip --version pip --version
virtualenv --version virtualenv --version
test -x /usr/local/sbin/runsvinit test -x /usr/local/sbin/runsvinit
test -d /etc/service test -d /etc/service
# The checks above leave caches behind (node compile cache etc). # The checks above leave caches and log files behind.
rm -rf /tmp/* /var/tmp/* /root/.cache /root/.npm /root/.config rm -rf /tmp/* /var/tmp/* /root/.cache /root/.npm /root/.config
rm -f /var/log/nginx/*
rm -f "$0" rm -f "$0"