feat(docker): migrate images to openSUSE Tumbleweed

This commit is contained in:
Pi Web Agent
2026-06-21 22:17:30 +00:00
parent 784a27a7ac
commit f8982a9947
10 changed files with 272 additions and 50 deletions
+1
View File
@@ -3,6 +3,7 @@
!Dockerfile
!bin/
!bin/hostexec
!bin/install-opensuse-base
!custom-image.d/
!custom-image.d/.gitkeep
!custom-image.d/*.sh
+27 -27
View File
@@ -1,22 +1,32 @@
# syntax=docker/dockerfile:1.7
ARG NODE_VERSION=22-bookworm-slim
ARG OPENSUSE_IMAGE=opensuse/tumbleweed
ARG DOCKER_CLI_VERSION=29-cli
FROM docker:${DOCKER_CLI_VERSION} AS docker-cli
FROM node:${NODE_VERSION} AS package
FROM ${OPENSUSE_IMAGE} AS base
ARG NODEJS_MAJOR=22
ARG NODEJS_REPO=auto
ARG PI_WEB_EXTRA_ZYPPER_PACKAGES=""
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ENV NPM_CONFIG_UPDATE_NOTIFIER=false \
SHELL=/bin/bash \
TERM=xterm-256color
COPY bin/install-opensuse-base /usr/local/sbin/install-pi-web-opensuse-base
RUN chmod 0755 /usr/local/sbin/install-pi-web-opensuse-base \
&& install-pi-web-opensuse-base
FROM base AS package
ARG PI_WEB_VERSION=latest
ARG PI_VERSION=latest
ARG CACHE_BUST=local
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates g++ make python3 \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \
echo "PI WEB Docker build cache bust: ${CACHE_BUST}"; \
npm install -g --omit=dev --no-audit --no-fund \
@@ -24,7 +34,7 @@ RUN set -eux; \
"@earendil-works/pi-coding-agent@${PI_VERSION}"; \
npm cache clean --force
FROM node:${NODE_VERSION} AS runtime
FROM base AS runtime
ENV NODE_ENV=production \
NPM_CONFIG_UPDATE_NOTIFIER=false \
@@ -39,18 +49,11 @@ ENV NODE_ENV=production \
SHELL=/bin/bash \
TERM=xterm-256color
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \
curl \
git \
openssh-client \
procps \
tini \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /data/home /data/config /data/pi-web /data/pi-agent /workspace \
&& chown -R node:node /data /workspace
COPY --from=package /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=package /usr/local/bin /usr/local/bin
COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker
COPY bin/hostexec /usr/local/bin/hostexec
RUN chmod 0755 /usr/local/bin/hostexec
COPY custom-image.d/ /tmp/pi-web-custom-image.d/
RUN bash -euxo pipefail -c '\
@@ -59,14 +62,11 @@ RUN bash -euxo pipefail -c '\
echo "Running PI WEB custom image hook: ${script}"; \
bash "${script}"; \
done; \
rm -rf /tmp/pi-web-custom-image.d /var/lib/apt/lists/* \
rm -rf /tmp/pi-web-custom-image.d; \
zypper clean --all; \
rm -rf /var/cache/zypp/* \
'
COPY --from=package /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=package /usr/local/bin /usr/local/bin
COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker
COPY --chmod=0755 bin/hostexec /usr/local/bin/hostexec
WORKDIR /workspace
USER node
+18 -18
View File
@@ -1,13 +1,20 @@
# syntax=docker/dockerfile:1.7
ARG NODE_VERSION=22-bookworm-slim
ARG OPENSUSE_IMAGE=opensuse/tumbleweed
ARG DOCKER_CLI_VERSION=29-cli
FROM docker:${DOCKER_CLI_VERSION} AS docker-cli
FROM node:${NODE_VERSION} AS dev
FROM ${OPENSUSE_IMAGE} AS dev
ARG NODEJS_MAJOR=22
ARG NODEJS_REPO=auto
ARG PI_WEB_EXTRA_ZYPPER_PACKAGES=""
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ENV NODE_ENV=development \
PATH=/workspace/node_modules/.bin:$PATH \
NPM_CONFIG_UPDATE_NOTIFIER=false \
NPM_CONFIG_CACHE=/data/npm-cache \
HOME=/data/home \
@@ -19,32 +26,23 @@ ENV NODE_ENV=development \
SHELL=/bin/bash \
TERM=xterm-256color
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \
curl \
g++ \
git \
make \
openssh-client \
procps \
python3 \
tini \
&& rm -rf /var/lib/apt/lists/*
COPY docker/bin/install-opensuse-base /usr/local/sbin/install-pi-web-opensuse-base
RUN chmod 0755 /usr/local/sbin/install-pi-web-opensuse-base \
&& install-pi-web-opensuse-base
WORKDIR /workspace
COPY package.json package-lock.json ./
COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs
RUN npm ci \
&& ln -sf /workspace/node_modules/.bin/pi /usr/local/bin/pi \
&& npm cache clean --force \
&& mkdir -p /data/home /data/config /data/npm-cache /data/pi-web /data/pi-agent \
&& chmod -R a+rwX /workspace/node_modules /data \
&& chmod 0777 /workspace
COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker
COPY --chmod=0755 docker/bin/hostexec /usr/local/bin/hostexec
COPY docker/bin/hostexec /usr/local/bin/hostexec
RUN chmod 0755 /usr/local/bin/hostexec
COPY docker/custom-image.d/ /tmp/pi-web-custom-image.d/
RUN bash -euxo pipefail -c '\
@@ -53,7 +51,9 @@ RUN bash -euxo pipefail -c '\
echo "Running PI WEB custom image hook: ${script}"; \
bash "${script}"; \
done; \
rm -rf /tmp/pi-web-custom-image.d /var/lib/apt/lists/* \
rm -rf /tmp/pi-web-custom-image.d; \
zypper clean --all; \
rm -rf /var/cache/zypp/* \
'
EXPOSE 8504 8505
+27 -3
View File
@@ -97,10 +97,27 @@ Common environment variables written to `.env`:
| `PI_WEB_BIND_ADDR`, `PI_WEB_PORT` | host bind address and port |
| `PI_WEB_VERSION` | npm version/range for `@jmfederico/pi-web` |
| `PI_VERSION` | npm version/range for `@earendil-works/pi-coding-agent` |
| `PI_WEB_OPENSUSE_IMAGE` | openSUSE base image used for the runtime build |
| `PI_WEB_NODEJS_MAJOR` | Node.js major package to install, defaulting to `22` |
| `PI_WEB_NODEJS_REPO` | Node.js zypper repository URL, `auto`, or `disabled` |
| `PI_WEB_EXTRA_ZYPPER_PACKAGES` | extra openSUSE packages installed during the image build |
| `PI_WEB_IMAGE` | local image tag to build and run |
| `HOSTEXEC_IMAGE` | helper image used by `hostexec` |
Host-derived IDs are refreshed on rerun unless you explicitly override them. User-facing values such as data directory, bind address, port, image names, upload limit, and version pins are preserved from an existing `.env` unless you pass a flag or environment override.
Host-derived IDs are refreshed on rerun unless you explicitly override them. User-facing values such as data directory, bind address, port, image names, upload limit, base image, Node.js settings, extra packages, and version pins are preserved from an existing `.env` unless you pass a flag or environment override.
### Base image and tooling
The Docker runtime and development images are openSUSE Tumbleweed based by default. They install Node.js 22, npm, `npx`, and Corepack through zypper, using the openSUSE Node.js build service repository when needed for the selected architecture. The image also includes common agent/development tools such as Git/Git LFS, GitHub CLI, OpenSSH, Python with pip/virtualenv and headers, native build tooling, `jq`, `ripgrep`, `fd`, `fzf`, `bat`, ShellCheck, archive tools, network utilities, and the Docker CLI.
Install extra distro packages without writing a hook by setting a whitespace-delimited package list:
```bash
PI_WEB_EXTRA_ZYPPER_PACKAGES="go rustup kubernetes-client" \
curl -fsSL https://raw.githubusercontent.com/jmfederico/pi-web/main/docker/install.sh | sh
```
You can also pass installer flags such as `--opensuse-image`, `--nodejs-major`, `--nodejs-repo`, and `--extra-zypper-packages`, or edit the generated `.env` and rerun the installer.
### Custom image hooks
@@ -110,13 +127,20 @@ The runtime image can be extended without changing PI WEB's Dockerfile. Put loca
~/.local/share/pi-web-docker/custom-image.d/
```
The installer preserves that directory, includes the `*.sh` files in the Docker build context, and runs each script as `root` during the image build in lexical order. Use this for optional tools such as `gh`, `glab`, `kubectl`, or cloud CLIs that you do not want in the default image.
The installer preserves that directory, includes the `*.sh` files in the Docker build context, and runs each script as `root` during the image build in lexical order. Use this for optional tools such as `glab`, `kubectl`, cloud CLIs, or language toolchains that you do not want in the default image.
Example:
```bash
mkdir -p ~/.local/share/pi-web-docker/custom-image.d
$EDITOR ~/.local/share/pi-web-docker/custom-image.d/10-github-cli.sh
cat >~/.local/share/pi-web-docker/custom-image.d/10-extra-tools.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
zypper --gpg-auto-import-keys --non-interactive refresh
zypper --non-interactive install --no-recommends glab kubernetes-client
zypper clean --all
EOF
chmod +x ~/.local/share/pi-web-docker/custom-image.d/10-extra-tools.sh
curl -fsSL https://raw.githubusercontent.com/jmfederico/pi-web/main/docker/install.sh | sh
```
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
set -euo pipefail
nodejs_major=${NODEJS_MAJOR:-22}
nodejs_repo=${NODEJS_REPO:-auto}
extra_zypper_packages=${PI_WEB_EXTRA_ZYPPER_PACKAGES:-}
nodejs_repo_flavor() {
local rpm_arch
rpm_arch=$(rpm --eval '%{_target_cpu}')
case "$rpm_arch" in
aarch64|armv6hl|armv7hl)
printf '%s\n' openSUSE_Factory_ARM
;;
ppc64le)
printf '%s\n' openSUSE_Factory_PowerPC
;;
riscv64)
printf '%s\n' openSUSE_Factory_RISCV
;;
s390x)
printf '%s\n' openSUSE_Factory_zSystems
;;
*)
printf '%s\n' openSUSE_Tumbleweed
;;
esac
}
add_nodejs_repo() {
local repo_url
case "$nodejs_repo" in
""|disabled|none)
return 0
;;
auto)
repo_url="https://download.opensuse.org/repositories/devel:/languages:/nodejs/$(nodejs_repo_flavor)/"
;;
*)
repo_url=$nodejs_repo
;;
esac
zypper --non-interactive removerepo pi-web-nodejs >/dev/null 2>&1 || true
zypper --non-interactive addrepo --refresh "$repo_url" pi-web-nodejs
}
# The codec repository is not needed for this image and can make noninteractive
# refreshes noisy or brittle when its signing key rolls independently.
zypper --non-interactive modifyrepo --disable repo-openh264 >/dev/null 2>&1 || true
add_nodejs_repo
zypper --gpg-auto-import-keys --non-interactive refresh
packages=(
"nodejs${nodejs_major}"
"npm${nodejs_major}"
"corepack${nodejs_major}"
"nodejs${nodejs_major}-devel"
bash
ca-certificates
curl
wget
git
git-lfs
gh
openssh-clients
procps
tini
shadow
gcc-c++
make
python3
python3-devel
python3-pip
python3-virtualenv
jq
ripgrep
fd
fzf
bat
ShellCheck
less
file
which
tar
gzip
xz
unzip
zip
zstd
findutils
grep
sed
gawk
patch
diffutils
util-linux
hostname
iproute2
bind-utils
rsync
)
extra_packages=()
if [ -n "$extra_zypper_packages" ]; then
# Intentionally split a whitespace-delimited package list supplied as a Docker
# build arg, e.g. PI_WEB_EXTRA_ZYPPER_PACKAGES="go rustup kubernetes-client".
# shellcheck disable=SC2206
extra_packages=($extra_zypper_packages)
fi
zypper --non-interactive install --no-recommends "${packages[@]}" "${extra_packages[@]}"
node --version
npm --version
npx --version
python3 --version
git --version
if ! getent group node >/dev/null 2>&1; then
groupadd --gid 1000 node
fi
if ! id node >/dev/null 2>&1; then
useradd --uid 1000 --gid node --create-home --home-dir /home/node --shell /bin/bash node
fi
mkdir -p /data/home /data/config /data/npm-cache /data/pi-web /data/pi-agent /workspace
chown -R node:node /data /workspace /home/node
zypper clean --all
rm -rf /var/cache/zypp/*
+5
View File
@@ -3,6 +3,11 @@ name: pi-web-dev
x-pi-web-dev-build: &pi-web-dev-build
context: ..
dockerfile: docker/Dockerfile.dev
args:
OPENSUSE_IMAGE: ${PI_WEB_OPENSUSE_IMAGE:-opensuse/tumbleweed}
NODEJS_MAJOR: ${PI_WEB_NODEJS_MAJOR:-22}
NODEJS_REPO: ${PI_WEB_NODEJS_REPO:-auto}
PI_WEB_EXTRA_ZYPPER_PACKAGES: ${PI_WEB_EXTRA_ZYPPER_PACKAGES:-}
x-pi-web-dev-environment: &pi-web-dev-environment
HOME: /data/home
+4
View File
@@ -4,6 +4,10 @@ x-pi-web-build: &pi-web-build
context: .
dockerfile: Dockerfile
args:
OPENSUSE_IMAGE: ${PI_WEB_OPENSUSE_IMAGE:-opensuse/tumbleweed}
NODEJS_MAJOR: ${PI_WEB_NODEJS_MAJOR:-22}
NODEJS_REPO: ${PI_WEB_NODEJS_REPO:-auto}
PI_WEB_EXTRA_ZYPPER_PACKAGES: ${PI_WEB_EXTRA_ZYPPER_PACKAGES:-}
PI_WEB_VERSION: ${PI_WEB_VERSION:-latest}
PI_VERSION: ${PI_VERSION:-latest}
CACHE_BUST: ${CACHE_BUST:-local}
+48
View File
@@ -28,6 +28,12 @@ Options:
--pi-web-version VER npm @jmfederico/pi-web version pin (default: latest)
--pi-version VER npm @earendil-works/pi-coding-agent version pin
(default: latest)
--opensuse-image IMAGE openSUSE base image (default: opensuse/tumbleweed)
--nodejs-major MAJOR Node.js major version package to install (default: 22)
--nodejs-repo REPO Node.js zypper repository URL, auto, or disabled
(default: auto)
--extra-zypper-packages LIST
extra openSUSE packages to install during image build
--asset-dir DIR Copy Docker assets from a local docker/ directory
--asset-ref REF Fetch Docker assets from a Git ref (default: main)
--skip-compose Write assets/.env but skip build and service recreate
@@ -72,6 +78,26 @@ while [ "$#" -gt 0 ]; do
PI_VERSION=$2
shift 2
;;
--opensuse-image)
[ "$#" -ge 2 ] || die "--opensuse-image requires a value"
PI_WEB_OPENSUSE_IMAGE=$2
shift 2
;;
--nodejs-major)
[ "$#" -ge 2 ] || die "--nodejs-major requires a value"
PI_WEB_NODEJS_MAJOR=$2
shift 2
;;
--nodejs-repo)
[ "$#" -ge 2 ] || die "--nodejs-repo requires a value"
PI_WEB_NODEJS_REPO=$2
shift 2
;;
--extra-zypper-packages)
[ "$#" -ge 2 ] || die "--extra-zypper-packages requires a value"
PI_WEB_EXTRA_ZYPPER_PACKAGES=$2
shift 2
;;
--asset-dir)
[ "$#" -ge 2 ] || die "--asset-dir requires a value"
PI_WEB_DOCKER_ASSET_DIR=$2
@@ -191,6 +217,12 @@ require_non_empty() {
[ -n "$value" ] || die "$name must not be empty"
}
dotenv_quote() {
value=$1
[ -n "$value" ] || return 0
printf '"%s"' "$(printf '%s' "$value" | sed 's/[\\"]/\\&/g')"
}
detect_docker_gid() {
if [ -S /var/run/docker.sock ]; then
if gid=$(stat -c '%g' /var/run/docker.sock 2>/dev/null); then
@@ -305,6 +337,7 @@ write_asset compose.yml 0644
write_asset .dockerignore 0644
write_asset install.sh 0755
write_asset bin/hostexec 0755
write_asset bin/install-opensuse-base 0755
custom_image_hooks_dir=$install_dir/custom-image.d
mkdir -p "$custom_image_hooks_dir" || die "could not create custom image hooks directory: $custom_image_hooks_dir"
@@ -323,6 +356,10 @@ pi_web_bind_addr=$(value_from_env_or_existing_or_default PI_WEB_BIND_ADDR 127.0.
pi_web_port=$(value_from_env_or_existing_or_default PI_WEB_PORT 8504)
pi_web_version=$(value_from_env_or_existing_or_default PI_WEB_VERSION latest)
pi_version=$(value_from_env_or_existing_or_default PI_VERSION latest)
pi_web_opensuse_image=$(value_from_env_or_existing_or_default PI_WEB_OPENSUSE_IMAGE opensuse/tumbleweed)
pi_web_nodejs_major=$(value_from_env_or_existing_or_default PI_WEB_NODEJS_MAJOR 22)
pi_web_nodejs_repo=$(value_from_env_or_existing_or_default PI_WEB_NODEJS_REPO auto)
pi_web_extra_zypper_packages=$(value_from_env_or_existing_or_default PI_WEB_EXTRA_ZYPPER_PACKAGES "")
pi_web_image=$(value_from_env_or_existing_or_default PI_WEB_IMAGE pi-web:local)
hostexec_image=$(value_from_env_or_existing_or_default HOSTEXEC_IMAGE alpine:3.22)
pi_web_max_upload_bytes=$(value_from_env_or_existing_or_default PI_WEB_MAX_UPLOAD_BYTES 67108864)
@@ -335,10 +372,15 @@ require_non_empty PI_WEB_BIND_ADDR "$pi_web_bind_addr"
require_non_empty PI_WEB_PORT "$pi_web_port"
require_non_empty PI_WEB_VERSION "$pi_web_version"
require_non_empty PI_VERSION "$pi_version"
require_non_empty PI_WEB_OPENSUSE_IMAGE "$pi_web_opensuse_image"
require_non_empty PI_WEB_NODEJS_MAJOR "$pi_web_nodejs_major"
require_non_empty PI_WEB_NODEJS_REPO "$pi_web_nodejs_repo"
require_non_empty PI_WEB_IMAGE "$pi_web_image"
require_non_empty HOSTEXEC_IMAGE "$hostexec_image"
require_non_empty PI_WEB_MAX_UPLOAD_BYTES "$pi_web_max_upload_bytes"
pi_web_extra_zypper_packages_env=$(dotenv_quote "$pi_web_extra_zypper_packages")
umask 077
temp_env=$env_file.$$
cat >"$temp_env" <<EOF
@@ -360,6 +402,12 @@ PI_WEB_PORT=$pi_web_port
PI_WEB_VERSION=$pi_web_version
PI_VERSION=$pi_version
# openSUSE/Node.js image build inputs.
PI_WEB_OPENSUSE_IMAGE=$pi_web_opensuse_image
PI_WEB_NODEJS_MAJOR=$pi_web_nodejs_major
PI_WEB_NODEJS_REPO=$pi_web_nodejs_repo
PI_WEB_EXTRA_ZYPPER_PACKAGES=$pi_web_extra_zypper_packages_env
# Runtime image names and limits.
PI_WEB_IMAGE=$pi_web_image
HOSTEXEC_IMAGE=$hostexec_image