From 89f835727b982846a8ae47b81d705ac68c3c85ff Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:42:33 +0100 Subject: [PATCH] chore: use Nix profiles in sandbox Docker image (#8140) * feat: use Nix profiles in sandbox Docker image Replace manual tool installs (rustup, nodesource, curl installers) in sandbox-image/Dockerfile.sandbox with a single `nix profile install .#sandbox`. All tools (Rust, Node, Bun, Deno, Go, gh, sqlx-cli, cargo-watch, Chromium, Playwright, etc.) are now managed declaratively via flake.nix. - Add `packages.sandbox` and `packages.sandbox-full` buildEnv outputs to flake.nix - Add `sandbox-env` helper script for browser tooling env vars - Update playwrightWrapper to export PLAYWRIGHT_BROWSERS_PATH - Rewrite Dockerfile.sandbox: Nix replaces ~50 lines of manual installs - Update entrypoint.sh to source Nix profile PATH - Delete deprecated root Dockerfile.sandbox Co-Authored-By: Claude Opus 4.6 * fix: sandbox image runs as non-root user with wmdev - Rewrite entrypoint.sh to start PostgreSQL as current user (no chown/su needed), fixing "Operation not permitted" when wmdev runs containers with --user - Add chmod -R 777 /root and passwd entry for UID 1000 so non-root containers can access bashrc, nix-profile, and tool configs - Remove apt postgresql server (Nix profile provides it) - Fix bash history expansion errors from literal `!` in system prompt - Fix asciinema path reference (available on PATH, not hardcoded) Co-Authored-By: Claude Opus 4.6 * fix: wrap pkg-config in sandbox profiles to bake in Nix search path Co-Authored-By: Claude Opus 4.6 * fix: add openssh-client and sudo to sandbox image for full root access Co-Authored-By: Claude Opus 4.5 * fix: use useradd instead of manual passwd entry for sandbox agent user Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .wmdev.yaml | 8 +- Dockerfile.sandbox | 231 ------------------------------- flake.nix | 49 +++++++ sandbox-image/Dockerfile.sandbox | 77 +++++------ sandbox-image/entrypoint.sh | 24 +++- 5 files changed, 100 insertions(+), 289 deletions(-) delete mode 100644 Dockerfile.sandbox diff --git a/.wmdev.yaml b/.wmdev.yaml index 5288efb82a..2b556fc0cc 100644 --- a/.wmdev.yaml +++ b/.wmdev.yaml @@ -53,12 +53,11 @@ profiles: --endpoint-url "$(printenv R2_ENDPOINT)" 3) The public URL will be: $(printenv R2_PUBLIC_URL)//screenshot.png - 4) Include screenshots in PR descriptions as markdown images: - ![description]($(printenv R2_PUBLIC_URL)//screenshot.png) + 4) Include in PR descriptions using markdown image syntax. --- Terminal Recordings (asciinema) --- You can record terminal sessions and upload them for sharing. - asciinema is pre-installed at /usr/local/bin/asciinema. + asciinema is available on PATH. 1) Write a shell script with the commands to demo. Add sleep delays for readable pacing: @@ -98,8 +97,7 @@ profiles: 4) The public URL will be: $(printenv R2_PUBLIC_URL)//diagram.svg - 5) Include in PR descriptions as markdown images: - ![description]($(printenv R2_PUBLIC_URL)//diagram.svg) + 5) Include in PR descriptions using markdown image syntax. linkedRepos: - repo: windmill-labs/windmill-ee-private diff --git a/Dockerfile.sandbox b/Dockerfile.sandbox deleted file mode 100644 index 64aa39855e..0000000000 --- a/Dockerfile.sandbox +++ /dev/null @@ -1,231 +0,0 @@ -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-* - - -# ── 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"] \ No newline at end of file diff --git a/flake.nix b/flake.nix index 34ecb689a1..135005b83d 100644 --- a/flake.nix +++ b/flake.nix @@ -342,11 +342,60 @@ # Wrapper for the Nix-provided playwright CLI (version-matched to its browsers) playwrightWrapper = pkgs.writeShellScriptBin "playwright" '' + export PLAYWRIGHT_BROWSERS_PATH="${pkgs.playwright-driver.browsers}" exec ${pkgs.nodejs}/bin/node ${pkgs.playwright-driver}/cli.js "$@" ''; + # --------------------------------------------------------------- + # sandbox-env script — outputs env vars for browser tooling + # Usage: eval "$(sandbox-env)" + # --------------------------------------------------------------- + + sandboxEnvScript = pkgs.writeShellScriptBin "sandbox-env" '' + echo "export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}" + echo "export PUPPETEER_EXECUTABLE_PATH=${pkgs.chromium}/bin/chromium" + echo "export PUPPETEER_SKIP_DOWNLOAD=true" + ''; + + # --------------------------------------------------------------- + # pkg-config wrapper — bakes in the Nix pkg-config search path + # so sandbox profiles (buildEnv) work without setting env vars. + # --------------------------------------------------------------- + + pkgConfigWrapper = pkgs.writeShellScriptBin "pkg-config" '' + export PKG_CONFIG_PATH="${pkgConfigPath}:$PKG_CONFIG_PATH" + exec ${pkgs.pkg-config}/bin/pkg-config "$@" + ''; + + # --------------------------------------------------------------- + # Installable sandbox profiles (nix profile install .#sandbox) + # --------------------------------------------------------------- + + sandboxEnv = pkgs.buildEnv { + name = "windmill-sandbox"; + paths = coreBuildInputs ++ helperScriptsBase + ++ [ playwrightWrapper sandboxEnvScript pkgConfigWrapper pkgs.chromium ]; + }; + + sandboxFullEnv = pkgs.buildEnv { + name = "windmill-sandbox-full"; + paths = coreBuildInputs ++ extraRuntimes + ++ helperScriptsBase ++ helperScriptsFull + ++ [ playwrightWrapper sandboxEnvScript pkgConfigWrapper pkgs.chromium + pkgs.cargo-sweep pkgs.xcaddy pkgs.nsjail ]; + }; + in { + # ============================================================= + # Installable profiles — for Docker / nix profile install + # Usage: nix profile install .#sandbox + # ============================================================= + + packages.sandbox = sandboxEnv; + packages.sandbox-full = sandboxFullEnv; + packages.default = sandboxEnv; + # ============================================================= # default — daily driver for backend + frontend development # Usage: nix develop diff --git a/sandbox-image/Dockerfile.sandbox b/sandbox-image/Dockerfile.sandbox index aaf71620d0..7c9ca181a4 100644 --- a/sandbox-image/Dockerfile.sandbox +++ b/sandbox-image/Dockerfile.sandbox @@ -1,61 +1,46 @@ FROM debian:bookworm-slim -# ── System packages ─────────────────────────────────────────────────────────── +# ── Minimal system deps ────────────────────────────────────────────────────── RUN apt-get update && apt-get install -y --no-install-recommends \ - curl ca-certificates git unzip openssh-client \ - # Rust native build deps - 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 - postgresql postgresql-client \ + ca-certificates curl git openssh-client sudo xz-utils \ + # PostgreSQL client (server provided by Nix profile) + postgresql-client \ && rm -rf /var/lib/apt/lists/* -# ── Node.js 22 ──────────────────────────────────────────────────────────────── -RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ - && apt-get install -y --no-install-recommends nodejs \ - && rm -rf /var/lib/apt/lists/* +# ── Nix (single-user, Determinate installer) ──────────────────────────────── +RUN curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | \ + sh -s -- install linux --no-confirm --init none +ENV PATH="/root/.nix-profile/bin:/nix/var/nix/profiles/default/bin:$PATH" -# ── GitHub CLI ──────────────────────────────────────────────────────────────── -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/* +# ── Install default sandbox profile ───────────────────────────────────────── +COPY flake.nix flake.lock /tmp/flake/ +RUN cd /tmp/flake && nix profile install .#sandbox \ + && rm -rf /tmp/flake -# ── Rust toolchain ──────────────────────────────────────────────────────────── -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ - sh -s -- -y --default-toolchain stable --profile minimal -ENV PATH="/root/.cargo/bin:$PATH" +# ── Browser env (Puppeteer / Mermaid point to Nix Chromium) ───────────────── +ENV PUPPETEER_SKIP_DOWNLOAD="true" +RUN printf '{"args":["--no-sandbox","--disable-setuid-sandbox"],"executablePath":"%s"}\n' \ + "$(readlink -f "$(which chromium)")" > /root/.puppeteerrc.json -RUN cargo install sqlx-cli --no-default-features --features native-tls,postgres \ - && cargo install cargo-watch - -# ── Bun ─────────────────────────────────────────────────────────────────────── -RUN curl -fsSL https://bun.sh/install | bash -ENV PATH="/root/.bun/bin:$PATH" - -# ── Playwright + Chromium ───────────────────────────────────────────────────── -RUN bun add -g @playwright/test \ - && bunx playwright install chromium --with-deps \ - && rm -rf /var/lib/apt/lists/* /tmp/bunx-* - -# ── Claude Code ─────────────────────────────────────────────────────────────── +# ── Claude Code ────────────────────────────────────────────────────────────── RUN curl -fsSL https://claude.ai/install.sh | bash ENV PATH="/root/.local/bin:$PATH" -# ── Codex ───────────────────────────────────────────────────────────────────── +# ── npm globals (install to /usr/local so bins land on PATH) ───────────────── +ENV NPM_CONFIG_PREFIX=/usr/local RUN npm i -g @openai/codex +RUN PUPPETEER_SKIP_DOWNLOAD=true npm i -g @mermaid-js/mermaid-cli -# ── Mermaid CLI ─────────────────────────────────────────────────────────────── -# Skip puppeteer's own Chromium download; reuse the one Playwright already installed above -RUN PUPPETEER_SKIP_DOWNLOAD=true npm i -g @mermaid-js/mermaid-cli \ - && CHROMIUM=$(find /root/.cache/ms-playwright -name 'chrome' -executable -type f | head -1) \ - && printf '{"args":["--no-sandbox","--disable-setuid-sandbox"],"executablePath":"%s"}\n' "$CHROMIUM" \ - > /root/.puppeteerrc.json +# ── Runtime env ────────────────────────────────────────────────────────────── +ENV CARGO_HOME=/tmp/.cargo -# ── Entrypoint (run explicitly via docker exec, not on container start) ────── -COPY entrypoint.sh /usr/local/bin/entrypoint.sh +# ── Allow non-root UID (--user) to access tools installed in /root ─────────── +# Give UID 1000 a proper passwd entry, passwordless sudo, and full access to /root +RUN chmod -R 777 /root \ + && useradd -u 1000 -g 100 -d /root -s /bin/bash -M agent \ + && echo "agent ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/agent \ + && chmod 0440 /etc/sudoers.d/agent + +# ── Entrypoint ─────────────────────────────────────────────────────────────── +COPY sandbox-image/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh diff --git a/sandbox-image/entrypoint.sh b/sandbox-image/entrypoint.sh index 24f384bcf5..b5a0e4b14e 100644 --- a/sandbox-image/entrypoint.sh +++ b/sandbox-image/entrypoint.sh @@ -1,21 +1,31 @@ #!/bin/bash set -euo pipefail -# ── Start PostgreSQL ────────────────────────────────────────────────────────── +# ── Nix profile ────────────────────────────────────────────────────────────── +export PATH="/root/.nix-profile/bin:/nix/var/nix/profiles/default/bin:$PATH" +export CARGO_HOME=/tmp/.cargo + +# ── Browser env (from Nix sandbox profile) ─────────────────────────────────── +if command -v sandbox-env >/dev/null 2>&1; then + eval "$(sandbox-env)" +fi + +# ── Start PostgreSQL as current user (no root/su needed) ───────────────────── PGDATA=/tmp/pgdata mkdir -p "$PGDATA" -chown postgres:postgres "$PGDATA" if [ ! -f "$PGDATA/PG_VERSION" ]; then - su - postgres -c "/usr/lib/postgresql/15/bin/initdb -D $PGDATA --auth=trust" + initdb -D "$PGDATA" --auth=trust fi -su - postgres -c "/usr/lib/postgresql/15/bin/pg_ctl -D $PGDATA -l /tmp/pg.log start -o '-k /tmp'" -su - postgres -c "psql -h /tmp -c 'CREATE ROLE root SUPERUSER LOGIN'" 2>/dev/null || true -su - postgres -c "createdb -h /tmp windmill" 2>/dev/null || true +pg_ctl -D "$PGDATA" -l /tmp/pg.log start -o "-k /tmp" + +# Create postgres role and windmill database (idempotent) +psql -h /tmp -d postgres -c "CREATE ROLE postgres SUPERUSER LOGIN" 2>/dev/null || true +createdb -h /tmp windmill 2>/dev/null || true # ── Run migrations if present ───────────────────────────────────────────────── if [ -d "$PWD/backend/migrations" ]; then - DATABASE_URL="postgres://root@localhost/windmill?host=/tmp" \ + DATABASE_URL="postgres://postgres@localhost/windmill?host=/tmp" \ sqlx migrate run --source "$PWD/backend/migrations" 2>/dev/null || true fi