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
+5
View File
@@ -0,0 +1,5 @@
---
"@jmfederico/pi-web": patch
---
Move the Docker runtime and development images to openSUSE Tumbleweed with Node.js 22, npx, Corepack, and common development tooling, plus zypper-based package customization.
+2 -2
View File
@@ -187,9 +187,9 @@ A Docker runtime is available for trusted local/server installs without using pr
curl -fsSL https://raw.githubusercontent.com/jmfederico/pi-web/main/docker/install.sh | sh
```
It builds a local image from npm, runs split `sessiond` and `web` services, binds the browser UI to `127.0.0.1:8504` by default, and uses the same command as the update path. The Docker setup intentionally mounts the Docker socket and selected host paths; treat it as root-equivalent host access, do not expose it directly to the public internet, and use an SSH tunnel, VPN, or authenticated reverse proxy for remote access.
It builds an openSUSE Tumbleweed based local image from npm with Node.js 22, npx, Corepack, and common development/agent tooling, runs split `sessiond` and `web` services, binds the browser UI to `127.0.0.1:8504` by default, and uses the same command as the update path. The Docker setup intentionally mounts the Docker socket and selected host paths; treat it as root-equivalent host access, do not expose it directly to the public internet, and use an SSH tunnel, VPN, or authenticated reverse proxy for remote access.
See the [Docker guide](https://github.com/jmfederico/pi-web/blob/main/docker/README.md) for trust warnings, version pinning, custom image hooks for optional CLIs, host command examples, and development Compose usage.
See the [Docker guide](https://github.com/jmfederico/pi-web/blob/main/docker/README.md) for trust warnings, version pinning, openSUSE package customization, custom image hooks for optional CLIs, host command examples, and development Compose usage.
PI WEB is also published as a Pi package. Installing it through Pi exposes a `/pi-web` command inside Pi:
+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