From dcb9e40ea237c3744611ac6e83ff5ecc6528e697 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 17 Jul 2026 15:44:57 +0200 Subject: [PATCH] fix(extra): make the extra container runnable as a non-root UID (#10173) * fix(extra): make extra container runnable as a non-root UID The root-run installs in DockerfileExtra inherit UV_CACHE_DIR (/tmp/windmill/cache/uv) and XDG_CACHE_HOME (/pyls/.cache) from the base image and write to them *after* the base image's world-writable pass, leaving ~320 root-owned 0755 dirs. A non-root UID then fails to debug: uv venv failed: Failed to write to the client cache Caused by: Permission denied (os error 13) at path "/tmp/windmill/cache/uv/simple-v21/pypi/.tmpWxWzTi" Re-apply the base image's world-writable convention after the installs, and add the windmill UID/GID 1000 account the app image already has so runAsUser: 1000 resolves to a real user with a writable $HOME. The entrypoint also wrote $NETRC to the 0700 /root under `set -e`, killing the container at startup; redirect $HOME when it is not writable. Verified against the published image as root, uid 1000, and uid 4567:0: all four services start and `windmill prepare-deps` succeeds in each. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(extra): scope the HOME fallback per-UID and cover non-root in CI Review findings on the previous commit: - `${HOME:-/root}` was used in the writability test but not the assignment, so an unset HOME (writable /root, redirect skipped) silently wrote the netrc to `/.netrc` instead of `/root/.netrc`. Normalize HOME once. - `mkdir -p` succeeds on a dir owned by another UID, so a root-created /tmp/windmill-home on a shared /tmp reintroduced the startup death this fix exists to prevent. Scope the fallback to /tmp/windmill-home-$(id -u). - The extra image's smoke test only ran as root, so nothing exercised the non-root path this PR is about. Add a --user 1000 run of the same suite plus a prepare-deps assertion, which is where the EACCES surfaced. Also correct the DockerfileExtra comment: the proven requirement is the uv cache, not runtime writes by gopls. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(extra): keep /tmp/monaco code read-only for non-root UIDs Review nit: /tmp/monaco holds node_modules (code, not cache), so it is not covered by the base image's world-writable cache convention. Give it a+rX files with 777 dirs -- enough for the go.mod / ruff.toml the entrypoint and pyls_launcher write into it -- instead of a+rw on every file. /tmp/windmill and /pyls/.cache keep a+rw: that is the base convention for caches (DockerfileSlimEe:69 chmod -R a+rw /tmp/windmill/cache; Dockerfile:339 "cache files already have 666"), and uv rewrites cache entries in place. Co-Authored-By: Claude Opus 4.8 (1M context) * chore(extra): drop the non-root CI smoke test Reverts publish_extra.yml to its root-only smoke test; the DockerfileExtra and entrypoint fix is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- docker/DockerfileExtra | 17 +++++++++++++---- docker/entrypoint-extra.sh | 25 +++++++++++++++++++------ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/docker/DockerfileExtra b/docker/DockerfileExtra index 190f65caba..b0e4bd8ad7 100644 --- a/docker/DockerfileExtra +++ b/docker/DockerfileExtra @@ -124,10 +124,19 @@ WORKDIR /app COPY docker/entrypoint-extra.sh /entrypoint.sh RUN chmod +x /entrypoint.sh -# Set permissions -RUN chmod -R a+rX /usr/local && \ - chmod -R a+rX /pyls && \ - chmod -R a+rX /debugger +# Non-root 'windmill' user with UID/GID 1000 to match the app image, so +# `runAsUser: 1000` resolves to a real account with a writable $HOME. +# No USER directive: the image still starts as root by default. +RUN addgroup --gid 1000 windmill && \ + adduser --disabled-password --gecos "" --uid 1000 --gid 1000 windmill + +# The root-run installs above write into the base image's UV_CACHE_DIR +# (/tmp/windmill/cache/uv) after the base already made it world-writable, leaving +# root-owned 0755 dirs that make uv fail EACCES for a non-root UID. /pyls/.cache +# (XDG_CACHE_HOME, incl. DENO_DIR) and /tmp/monaco are written at runtime too. +RUN chmod -R a+rX /usr/local /pyls /debugger /multiplayer /tmp/monaco && \ + chmod -R a+rw /tmp/windmill /pyls/.cache && \ + find /tmp/windmill /pyls/.cache /tmp/monaco -type d -exec chmod 777 {} + # Expose all service ports EXPOSE 3000 3001 3002 3003 diff --git a/docker/entrypoint-extra.sh b/docker/entrypoint-extra.sh index 385db124fb..a06901e25c 100644 --- a/docker/entrypoint-extra.sh +++ b/docker/entrypoint-extra.sh @@ -21,16 +21,29 @@ cleanup() { trap cleanup SIGTERM SIGINT +# An arbitrary non-root UID gets HOME=/ and cannot write the image's 0700 /root, so +# redirect $HOME before anything writes under it (netrc below, plus the bun/npm/go +# caches in the services). Keep the fallback UID-scoped: a leftover dir from a +# different UID on a shared /tmp is not writable. Root keeps HOME=/root. +HOME="${HOME:-/root}" +if [ ! -w "$HOME" ]; then + echo "[entrypoint] HOME=$HOME is not writable for UID $(id -u), using HOME=/tmp/windmill-home-$(id -u)" + HOME="/tmp/windmill-home-$(id -u)" + mkdir -p "$HOME" +fi +export HOME + # Setup NETRC if provided (for LSP) if [ -n "$NETRC" ]; then - echo "$NETRC" > /root/.netrc - chmod 600 /root/.netrc + echo "$NETRC" > "$HOME/.netrc" + chmod 600 "$HOME/.netrc" fi -# Setup cache directory for LSP -if [ -d /root/.cache ]; then - export XDG_CACHE_HOME=/root/.cache - cp -r /pyls/.cache /root/.cache 2>/dev/null || true +# Setup cache directory for LSP (falls back to the image's world-writable +# XDG_CACHE_HOME=/pyls/.cache when $HOME/.cache isn't mounted) +if [ -d "$HOME/.cache" ]; then + export XDG_CACHE_HOME="$HOME/.cache" + cp -r /pyls/.cache "$HOME/.cache" 2>/dev/null || true fi # Setup Monaco temp directory for LSP