Files
windmill/docker/DockerfileExtra
T
Ruben Fiszel dcb9e40ea2 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>
2026-07-17 15:44:57 +02:00

167 lines
5.6 KiB
Plaintext

# DockerfileExtra - Combined Windmill Extra Services
#
# This image extends windmill-slim with three optional Windmill services:
# - LSP (Language Server Protocol) - Port 3001
# - Multiplayer (y-websocket) - Port 3002
# - Debugger (DAP WebSocket) - Port 3003
#
# Each service can be enabled/disabled via environment variables:
# - ENABLE_LSP=true (default: true)
# - ENABLE_MULTIPLAYER=true (default: true)
# - ENABLE_DEBUGGER=true (default: true)
#
# Build:
# docker build -f docker/DockerfileExtra -t windmill-extra .
#
# Run:
# docker run -p 3001:3001 -p 3002:3002 -p 3003:3003 windmill-extra
# ============================================================================
# Build final extra services image from windmill-ee-slim (includes nsjail)
# ============================================================================
FROM ghcr.io/windmill-labs/windmill-ee-slim:latest AS final
ARG APP=/usr/src/app
# Install Node.js 22 (needed for LSP and multiplayer)
RUN apt-get update && apt-get install -y --no-install-recommends \
gnupg \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install additional system dependencies
# - shellcheck: for bash LSP
RUN apt-get update && apt-get install -y --no-install-recommends \
shellcheck \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install Go for gopls (Go LSP)
RUN set -eux; \
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
case "$arch" in \
'amd64') targz='go1.26.0.linux-amd64.tar.gz' ;; \
'arm64') targz='go1.26.0.linux-arm64.tar.gz' ;; \
'armhf') targz='go1.26.0.linux-armv6l.tar.gz' ;; \
*) echo >&2 "error: unsupported architecture '$arch'"; exit 1 ;; \
esac; \
wget "https://golang.org/dl/$targz" -nv && tar -C /usr/local -xzf "$targz" && rm "$targz"
ENV PATH="${PATH}:/usr/local/go/bin"
ENV GOBIN=/usr/local/go/bin
# Install gopls for Go LSP
RUN /usr/local/go/bin/go install -v golang.org/x/tools/gopls@latest
# Copy Deno for Deno LSP
COPY --from=denoland/deno:2.2.1 --chmod=755 /usr/bin/deno /usr/bin/deno
# ============================================================================
# LSP Setup
# ============================================================================
ENV PIPENV_VENV_IN_PROJECT=1
ENV XDG_CACHE_HOME=/pyls/.cache
# Install Python packages for LSP using uv
RUN uv pip install --system --break-system-packages pipenv tornado python-lsp-jsonrpc ruff Cython
# Install Node-based language servers
RUN npm install -g diagnostic-languageserver pyright
# Setup LSP working directory
WORKDIR /pyls
COPY lsp/Pipfile .
RUN pipenv install
COPY lsp/pyls_launcher.py .
# Setup Monaco temp directory for LSP
RUN mkdir -p /tmp/monaco && chmod -R 777 /tmp/monaco
RUN cd /tmp/monaco && npm install --save-dev windmill-client
RUN mkdir -p /pyls/.cache
# ============================================================================
# Debugger Setup
# ============================================================================
WORKDIR /debugger
# Copy debugger files
COPY debugger/dap_debug_service.ts .
COPY debugger/dap_websocket_server_bun.ts .
COPY debugger/dap_websocket_server.py .
COPY debugger/nsjail.debug.config.proto .
# Install Python debugger dependencies using uv
RUN uv pip install --system --break-system-packages websockets debugpy
# ============================================================================
# Multiplayer Setup (y-websocket with connection logging)
# ============================================================================
WORKDIR /multiplayer
# Copy multiplayer server files
COPY multiplayer/package.json .
COPY multiplayer/server.mjs .
COPY multiplayer/gateway.mjs .
# Install dependencies
RUN npm install
# ============================================================================
# Entrypoint Setup
# ============================================================================
WORKDIR /app
# Copy entrypoint script
COPY docker/entrypoint-extra.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# 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
# Environment variables for service control
ENV ENABLE_LSP=true
ENV ENABLE_MULTIPLAYER=true
ENV ENABLE_DEBUGGER=true
# nsjail sandboxing for debugger (requires --privileged, off by default)
ENV ENABLE_GATEWAY=true
ENV GATEWAY_PORT=3000
ENV ENABLE_NSJAIL=false
# LSP port
ENV LSP_PORT=3001
# Multiplayer port and host
ENV MULTIPLAYER_PORT=3002
ENV HOST=0.0.0.0
# Debugger port
ENV DEBUGGER_PORT=3003
# Windmill base URL for debugger token verification
ENV WINDMILL_BASE_URL=""
ENTRYPOINT ["/entrypoint.sh"]