FROM debian:bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ ca-certificates \ git \ iptables \ gosu \ sudo \ unzip \ # Rust native build deps (for cargo check) pkg-config \ cmake \ clang \ mold \ libtool \ libssl-dev \ libxml2-dev \ libxmlsec1-dev \ libxslt1-dev \ libffi-dev \ zlib1g-dev \ libcurl4-openssl-dev \ libclang-dev \ libkrb5-dev \ libsasl2-dev \ # PostgreSQL (for local DB during development) postgresql \ postgresql-client \ # Node.js 22 (for npm run check / frontend dev) && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && rm -rf /var/lib/apt/lists/* \ # Container runs as arbitrary UIDs (--user uid:gid). These three lines make # sudo work for any UID: # 1) NOPASSWD rule so sudo never prompts for a password # 2) Writable passwd/group so the entrypoint can register the dynamic UID # 3) Writable shadow so unix_chkpwd can validate the account (without this, # sudo fails with "account validation failure, is your account locked?") && echo "ALL ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/sandbox \ && chmod 0440 /etc/sudoers.d/sandbox \ && chmod 666 /etc/passwd /etc/group /etc/shadow # ── GitHub CLI (for PR creation) ────────────────────────────────────────────── RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ > /etc/apt/sources.list.d/github-cli.list \ && apt-get update && apt-get install -y --no-install-recommends gh \ && rm -rf /var/lib/apt/lists/* # ── Rust toolchain ──────────────────────────────────────────────────────────── # Install under /usr/local/lib/ so bins are world-readable with default umask. # CARGO_HOME is overridden to /tmp/.cargo at the end for mutable runtime state. ENV RUSTUP_HOME=/usr/local/lib/rustup CARGO_HOME=/usr/local/lib/cargo RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ sh -s -- -y --default-toolchain stable --profile minimal && \ ln -s /usr/local/lib/cargo/bin/* /usr/local/bin/ RUN cargo install sqlx-cli --no-default-features --features native-tls,postgres && \ cargo install cargo-watch && \ cargo install --locked --git https://github.com/asciinema/asciinema && \ ln -sf /usr/local/lib/cargo/bin/sqlx /usr/local/bin/sqlx && \ ln -sf /usr/local/lib/cargo/bin/cargo-watch /usr/local/bin/cargo-watch && \ ln -sf /usr/local/lib/cargo/bin/asciinema /usr/local/bin/asciinema # ── Register dynamic runtime users ─────────────────────────────────────────── RUN cat <<'SCRIPT' > /usr/local/bin/register-dynamic-user.sh #!/bin/sh set -eu uid="${1:-}" gid="${2:-}" if [ -z "$uid" ] || [ -z "$gid" ]; then echo "register-dynamic-user: usage: register-dynamic-user " >&2 exit 1 fi if ! getent group "$gid" >/dev/null 2>&1; then echo "sandbox:x:${gid}:" >> /etc/group fi if ! getent passwd "$uid" >/dev/null 2>&1; then echo "sandbox:x:${uid}:${gid}:sandbox:/tmp:/bin/sh" >> /etc/passwd fi # Add a shadow entry ("*" = no password) so unix_chkpwd doesn't reject sudo. if ! grep -q "^sandbox:" /etc/shadow 2>/dev/null; then echo "sandbox:*:19000:0:99999:7:::" >> /etc/shadow fi SCRIPT RUN chmod +x /usr/local/bin/register-dynamic-user.sh # ── Network init script (iptables firewall + privilege drop) ────────────────── RUN cat <<'SCRIPT' > /usr/local/bin/network-init.sh #!/bin/bash set -euo pipefail if [ -n "${WM_PROXY_HOST:-}" ] && [ -n "${WM_PROXY_PORT:-}" ]; then # Resolve hostnames to ALL IPs (multi-A records, round-robin DNS) PROXY_IPS=$(getent ahostsv4 "$WM_PROXY_HOST" | awk '{print $1}' | sort -u) RPC_HOST="${WM_RPC_HOST:-$WM_PROXY_HOST}" RPC_IPS=$(getent ahostsv4 "$RPC_HOST" | awk '{print $1}' | sort -u) if [ -z "$PROXY_IPS" ] || [ -z "$RPC_IPS" ]; then echo "network-init: failed to resolve proxy/RPC host" >&2 exit 1 fi # IPv4: default deny outbound iptables -P OUTPUT DROP iptables -A OUTPUT -o lo -j ACCEPT iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # Allow DNS (UDP/TCP 53) to configured nameservers. if [ -f /etc/resolv.conf ]; then grep '^nameserver' /etc/resolv.conf | awk '{print $2}' | while read -r ns; do iptables -A OUTPUT -d "$ns" -p udp --dport 53 -j ACCEPT iptables -A OUTPUT -d "$ns" -p tcp --dport 53 -j ACCEPT done fi # Allow ALL resolved proxy IPs (handles multi-A DNS) for ip in $PROXY_IPS; do iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_PROXY_PORT" -j ACCEPT done # Allow ALL resolved RPC IPs if [ -n "${WM_RPC_PORT:-}" ]; then for ip in $RPC_IPS; do iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_RPC_PORT" -j ACCEPT done fi # Reject (not drop) everything else to fail fast instead of hanging iptables -A OUTPUT -j REJECT # IPv6: block entirely to prevent leaks (fail closed) if ip6tables -L -n >/dev/null 2>&1; then ip6tables -P OUTPUT DROP ip6tables -A OUTPUT -o lo -j ACCEPT ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT ip6tables -A OUTPUT -j REJECT else if ! sysctl -w net.ipv6.conf.all.disable_ipv6=1 2>/dev/null; then echo "network-init: failed to block IPv6 (neither ip6tables nor sysctl available)" >&2 exit 1 fi fi fi # Add sandbox user/group so sudo works after dropping privileges. if [ -z "${WM_TARGET_UID:-}" ] || [ -z "${WM_TARGET_GID:-}" ]; then echo "network-init: WM_TARGET_UID and WM_TARGET_GID are required" >&2 exit 1 fi /usr/local/bin/register-dynamic-user.sh "${WM_TARGET_UID}" "${WM_TARGET_GID}" # Fix PTY ownership so the unprivileged user can read/write the terminal. if [ -t 0 ]; then chown "${WM_TARGET_UID}:${WM_TARGET_GID}" "$(tty)" fi # Drop privileges and exec the user command. exec gosu "${WM_TARGET_UID}:${WM_TARGET_GID}" env HOME=/tmp "$@" SCRIPT RUN chmod +x /usr/local/bin/network-init.sh # ── workmux (sandbox RPC) ──────────────────────────────────────────────────── RUN curl -fsSL https://raw.githubusercontent.com/raine/workmux/main/scripts/install.sh | bash # ── Claude Code ─────────────────────────────────────────────────────────────── RUN curl -fsSL https://claude.ai/install.sh | bash && \ target="$(readlink -f /root/.local/bin/claude)" && \ mv /root/.local/share/claude /usr/local/lib/claude && \ ln -s "/usr/local/lib/claude/versions/$(basename "$target")" /usr/local/bin/claude && \ mkdir -p /tmp/.local/bin && \ ln -s /usr/local/bin/claude /tmp/.local/bin/claude && \ chmod -R a+rwX /tmp/.local # ── Codex ───────────────────────────────────────────────────────────────────── RUN npm i -g @openai/codex # ── Bun ─────────────────────────────────────────────────────────────────────── ENV BUN_INSTALL=/usr/local/lib/bun RUN curl -fsSL https://bun.sh/install | bash && \ ln -s /usr/local/lib/bun/bin/bun /usr/local/bin/bun && \ ln -s /usr/local/lib/bun/bin/bunx /usr/local/bin/bunx # ── Playwright + Chromium (for screenshots) ────────────────────────────────── ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/lib/playwright-browsers RUN bun add -g @playwright/test \ && bunx playwright install chromium --with-deps \ && chmod -R a+rwX /usr/local/lib/playwright-browsers \ && chmod -R a+rwX /usr/local/lib/bun/install \ && rm -rf /var/lib/apt/lists/* /tmp/bunx-* # ── AWS CLI (for S3-compatible uploads to R2) ───────────────────────────────── RUN curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip \ && unzip -q /tmp/awscliv2.zip -d /tmp \ && /tmp/aws/install \ && rm -rf /tmp/aws /tmp/awscliv2.zip ENV AWS_DEFAULT_REGION=auto # ── Runtime env for arbitrary UID ───────────────────────────────────────────── # Mutable state goes to /tmp (writable by any UID). Toolchains stay read-only. ENV CARGO_HOME=/tmp/.cargo BUN_TMPDIR=/tmp # ── Entrypoint ──────────────────────────────────────────────────────────────── RUN cat <<'ENTRY' > /usr/local/bin/entrypoint.sh #!/bin/sh /usr/local/bin/register-dynamic-user.sh "$(id -u)" "$(id -g)" # Start PostgreSQL (unix socket in /tmp, owned by postgres user) mkdir -p /tmp/pgdata && sudo chown postgres:postgres /tmp/pgdata if [ ! -f /tmp/pgdata/PG_VERSION ]; then sudo -u postgres /usr/lib/postgresql/15/bin/initdb -D /tmp/pgdata --auth=trust fi sudo -u postgres /usr/lib/postgresql/15/bin/pg_ctl -D /tmp/pgdata -l /tmp/pg.log start -o "-k /tmp" sudo -u postgres psql -h /tmp -c "CREATE ROLE sandbox SUPERUSER LOGIN" 2>/dev/null || true sudo -u postgres createdb -h /tmp windmill 2>/dev/null || true # Run database migrations so sqlx compile-time checks work if [ -d "$PWD/backend/migrations" ]; then DATABASE_URL="postgres://sandbox@localhost/windmill?host=/tmp" \ sqlx migrate run --source "$PWD/backend/migrations" 2>/dev/null || true fi # Install frontend dependencies and generate backend client if [ -d "$PWD/frontend" ]; then (cd "$PWD/frontend" && npm install && npm run generate-backend-client) 2>/dev/null || true fi exec "$@" ENTRY RUN chmod +x /usr/local/bin/entrypoint.sh ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]