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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-07-17 15:44:57 +02:00
committed by GitHub
parent 97f4477069
commit dcb9e40ea2
2 changed files with 32 additions and 10 deletions
+13 -4
View File
@@ -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
+19 -6
View File
@@ -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