Compare commits

...
Author SHA1 Message Date
Ruben FiszelandClaude Opus 4.8 c8e4f31749 refactor(worker): drop docker image storage cap; scope rootless netns per-job
Remove the `docker_image_storage_size_mb` instance setting and the polling
storage monitor entirely. Now that `# docker` jobs are refused under nsjail
(the per-job podman daemon runs outside the sandbox), the asymmetry that
justified a docker-specific disk cap is gone: a normal job can already exhaust
host disk in non-nsjail modes, so a docker-only cap was inconsistent. Docker
jobs now use disk like any other job — bound it at the infra level.

Also scope the rootless slirp4netns network-backend override to the per-job
podman instance via a job-scoped `$HOME` containers.conf instead of a global
`/etc/containers` drop-in, so rootful podman elsewhere in the *-full image is
unaffected (flagged by cubic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:15:39 +00:00
Ruben FiszelandClaude Opus 4.8 6269b1e86f docs(worker): note world-readable /proc (cmdline/args) exposure of docker jobs
The per-job-podman bind-mount escape can read world-readable host state — including
world-readable /proc (process cmdlines/args via `-v /proc`), other job dirs'
world-readable files, and the dep cache — but NOT 0400 files like /proc/<pid>/environ
(env secrets stay protected on a root worker). Document this in the security notes
(README + compose + code comment) with the corollary: don't pass secrets as
command-line args (windmill uses env). Verified: uid-1000 container via `-v /proc`
reading a root process's environ -> 'Permission denied'; its cmdline -> readable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 01:48:57 +00:00
Ruben FiszelandClaude Opus 4.8 cd1930c06a feat(worker): don't auto-provide per-job podman for docker jobs under nsjail
Enabling nsjail is an explicit 'fully sandbox jobs' signal, but the per-job podman
daemon runs OUTSIDE the jail and a # docker script can bind-mount worker-visible
paths through it — silently providing it under nsjail would be a surprising hole in
a setup the operator hardened on purpose. So refuse instead: under nsjail, a
# docker job with no explicitly-provided Docker daemon errors with guidance to use
DOCKER_HOST (a network-reachable daemon; a mounted unix socket isn't reachable in
the jail) or a non-nsjail worker group. nsjail is off by default (DISABLE_NSJAIL
defaults true → default workers use unshare/none), so docker-via-podman keeps
working out of the box; this only gates the explicit-nsjail case. Docs updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 01:39:49 +00:00
Ruben FiszelandClaude Opus 4.8 e1856d30ec feat(worker): drop per-job podman to non-root when worker is root (protect /proc env)
When the docker worker runs as root (the default for compose/helm workers), start
the per-job podman via `runuser` dropped to a non-root uid so it runs rootless.
This keeps the worker's secrets in /proc/<worker>/environ (DATABASE_URL etc.)
root-owned and therefore unreadable by a `docker run --pid=host` container, and
makes a container escape land unprivileged — without the fragile namespace
confinement (no unshare/mask/pid-ns, which conflicted with podman system service).

A non-root worker can't drop further (podman runs rootless as itself; a --pid=host
container shares its uid), so run the docker worker as root for the /proc protection.
The per-job podman + its containers are spawned in a new process group so teardown
and the storage-cap monitor kill the whole tree (podman is a runuser grandchild).

This does NOT confine the container's filesystem view — a `# docker` script can
still bind-mount worker-visible paths — so docker-capable workers remain a
trusted-tenant capability (documented). Reverses the earlier `user: 1000` advice:
keep the worker root; windmill drops podman itself.

e2e verified (bare-metal, worker as root): stock docker job runs (dropped rootless
podman); a uid-1000 --pid=host container reading the root worker's /proc/environ ->
'Permission denied' (DATABASE_URL/secret protected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 01:33:51 +00:00
Ruben FiszelandClaude Opus 4.8 f1b6746e0e fix(worker): address docker review items (hot-reload, nsjail socket gate, async teardown)
- P2: hot-reload docker_image_storage_size_mb — handle the setting in
  process_notify_event (main.rs), not just initial_load, so the instance-settings
  UI change takes effect on running workers without a restart.
- P2: don't let a mounted host /var/run/docker.sock suppress per-job podman under
  nsjail — the bash nsjail proto never mounts the host socket into the jail, so it's
  unreachable there; only count the unix socket when !nsjail (DOCKER_HOST tcp still
  counts in all modes since the network crosses the jail).
- P1: make PerJobPodman teardown non-blocking — offload `podman system reset` +
  service kill + rmdir to spawn_blocking (bounded by `timeout 30`) instead of
  blocking the Tokio worker thread in Drop; run inline only when off-runtime.
- Docs: note that the per-job podman daemon runs outside nsjail (Docker-API bind
  mounts can reach worker-visible paths — trusted-tenant capability, prefer
  dedicated docker workers on shared fleets), and that mixed base/full fleets must
  route docker jobs to a *-full WORKER_TAGS=docker group rather than the default tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:04:42 +00:00
Ruben FiszelandClaude Opus 4.8 a1efca2fb5 feat(frontend): allow Docker scripts on the multi-tenant cloud
Docker mode no longer relies on a privileged dind sidecar / host socket — each
# docker job runs in its own ephemeral rootless podman, torn down with the job,
with a per-job image-storage cap. So the hard block on app.windmill.dev is no
longer warranted. Remove the isCloudHosted() gate (+ now-unused imports) from the
script and flow inline-script pickers so Docker is selectable on cloud like any
other language. Requires the cloud worker fleet to run docker-capable workers
(*-full image + rootless podman) serving the auto-assigned 'docker' tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:23:50 +00:00
Ruben FiszelandClaude Opus 4.8 f245b2bbbc feat(worker): auto 'docker' tag for # docker jobs + soft image-storage cap
Tag-only routing (no new ScriptLang — the bash executor handles the annotation):
a Bash script with the '# docker' annotation and no explicit tag is auto-tagged
'docker' at script-create (stored on script.tag, before no-op detection) and at
preview push. 'docker' is added to DEFAULT_TAGS so default workers serve it out of
the box; run a worker group with WORKER_TAGS=docker to route docker jobs to
dedicated/bigger workers. Mirrors the routing half of bunnative/nativets.

Soft size cap for the per-job rootless-podman image store via a new instance
setting docker_image_storage_size_mb (default 8GB; 0 = uncapped). A background
monitor polls the graphroot ('podman unshare du', robust to subuid-owned overlay
layers) and, past the cap, logs a clear error and tears the runtime down (kills the
service so an in-flight pull fails, 'system reset' to stop containers + free space).
This is the rootless-compatible enforcement: a uid-1000 worker cannot mount a sized
tmpfs even when privileged, so a kernel hard-cap isn't available; soft (overshoot
up to one ~2s poll). Without this, # docker jobs could fill the worker disk under
nsjail, unlike other languages capped by the nsjail tmpfs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:20:37 +00:00
Ruben FiszelandClaude Opus 4.8 3e9f636aae docs(docker): enable # docker on the default worker (full image + /dev/fuse)
Replace the dedicated windmill_worker_docker group example with guidance on the
default worker: since # docker scripts are tagged "bash" they already run there,
so the only requirements are a *-full image (ships podman) and the /dev/fuse
device — no dedicated group or custom tag. Also note the *-full image's bundled
runtimes (Java, .NET, Ruby, R, Rust, Ansible, Nushell; Oracle/Kerberos in EE).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:23:13 +00:00
Ruben FiszelandClaude Opus 4.8 2b1ee01b42 docs(worker): fix stale comments after per-job/auto-detect refactor
Comment-only cleanup flagged in review:
- PerJobPodman struct doc: it's used for docker jobs in ANY sandbox mode (not just
  nsjail), and teardown is `podman system reset` (removes images + subuid-owned
  layers), not `rm -af` + kill service.
- connect_docker / handle_docker_job param comments: drop the "(nsjail path)" — per-job
  podman runs in all modes now.
- DockerfileFull/FullEe: the container_runtime config was removed; podman is started
  automatically when no Docker daemon is provided.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:54:03 +00:00
Ruben FiszelandClaude Opus 4.8 cccd8b9847 refactor(worker): auto-select podman vs provided Docker daemon; drop container_runtime config
Remove the per-worker-group container_runtime option entirely and decide the docker
runtime automatically: for a `# docker` job, if a Docker daemon is already provided
(DOCKER_HOST set or /var/run/docker.sock mounted) use it (backwards compatible,
unchanged); otherwise start a per-job rootless podman (its own ephemeral daemon,
torn down with the job). podman must be present (the *-full images) — else a clear
error.

Removed: container_runtime from WorkerConfigOpt/WorkerConfig/load_worker_config/
WORKER_CONFIG/monitor, the CE config allowlist entry, and the frontend "Container
runtime" toggle. docker-compose / README: the windmill_worker_docker example no
longer sets CONTAINER_RUNTIME (podman is automatic when no daemon is provided).

Verified e2e: a worker with no CONTAINER_RUNTIME and no DOCKER_HOST auto-runs a
# docker job via per-job podman; the provided-daemon (legacy) path is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:45:13 +00:00
Ruben FiszelandClaude Opus 4.8 40a387a17b docs+build: drop legacy dind from compose, fix full-image podman prereqs
docker-compose.yml / README: remove the legacy dind sidecar and the
DOCKER_HOST/host-socket opt-in comments (confusing now that podman is the way).
Present a single clean path: a dedicated windmill_worker_docker group with the
rootless podman runtime (full image, user 1000, privileged + /dev/fuse,
CONTAINER_RUNTIME=podman, WORKER_TAGS=docker). `# docker` scripts are tagged by
language ("bash"), so routing is via a custom "docker" tag — documented. The
legacy externally-provided DOCKER_HOST / mounted /var/run/docker.sock still works
in the backend; it's just no longer advertised in the quickstart.

DockerfileFull/FullEe: two fixes required for podman-in-container (found by
testing a containerized worker):
- chmod u+s newuidmap/newgidmap — rootless subuid mapping fails if the package's
  file caps are lost in image layers.
- default rootless networking to slirp4netns (containers.conf.d) — the netavark
  bridge default fails rootless on hosts without the needed nftables setup.

Verified e2e (podman-in-docker): a containerized worker ran a # docker job via
per-job rootless podman with both fixes + privileged + /dev/fuse; confinement held.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:30:25 +00:00
Ruben FiszelandClaude Opus 4.8 f61cf1633b refactor(worker): per-job podman for all docker jobs, drop the shared service
Generalize the per-job rootless podman instance to every docker job (any sandbox
mode) instead of only nsjail, and remove the long-lived shared service:

- Drop start_container_runtime_maybe() + its worker-startup call + the global
  DOCKER_HOST mutation. Each docker job now spins up its own ephemeral per-job
  podman (start_per_job_podman) and tears it down at job end — so no container
  outlives any job, and there's no standing shared socket on the worker.
- per_job_podman is gated strictly on container_runtime=podman. The legacy path
  (externally-provided DOCKER_HOST or a mounted /var/run/docker.sock, without
  container_runtime=podman) is unchanged: connect_docker(None)/forwarded env,
  and Windmill only manages the $WM_JOB_ID container as before.
- docker_host handed to the script: the in-jail /tmp/podman.sock under nsjail
  (bind-mounted), else the per-job host socket directly (unshare/none).
- The "no Docker daemon reachable" hint no longer fires when
  container_runtime=podman (per-job provides the daemon).
- monitor: drop the container_runtime killpill — with no startup service the
  config is read live per job, no worker restart needed on change.

Verified e2e against rootless podman: docker jobs work + confine extra containers
under nsjail AND unshare; legacy host-socket jobs run against the host daemon
with no per-job podman and extras persisting (unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:11:20 +00:00
Ruben FiszelandClaude Opus 4.8 9a64e74aa2 fix(worker): use podman system reset to tear down per-job store (no storage leak)
The per-job podman teardown used `podman rm -af` (containers only) + remove_dir_all.
But rootless podman stores image layers under remapped subuids, which the worker
user cannot delete — so any image the job pulled leaked its overlay layers under
the job dir (and blocked the job-dir cleanup too). Use `podman system reset
--force`, which removes containers AND images and deletes the subuid-owned layers
from inside the user namespace, so remove_dir_all then succeeds.

Verified e2e (nsjail + rootless podman): a job pulling an image now leaves no
subuid-owned files and no per-job podman dir after teardown; container confinement
unchanged (an extra detached container is still killed with the job).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:38:13 +00:00
Ruben FiszelandClaude Opus 4.8 8483700f0e feat(worker): confine docker containers to the job under nsjail
Under nsjail (untrusted), a docker job now runs against its OWN rootless podman
instance instead of the shared per-worker service, so no container it spawns can
outlive the job — even extra/detached ones the script starts.

- start_per_job_podman(): per docker+nsjail job (when container_runtime=podman),
  start a rootless `podman system service` with isolated --root/--runroot under
  the job dir; its unix socket is bind-mounted into the jail ({DOCKER_SOCK_MOUNT}
  in run.bash.config.proto) and handed to the script as DOCKER_HOST.
- connect_docker()/handle_docker_job() take an optional per-job docker_host so
  bollard talks to the per-job socket.
- PerJobPodman Drop tears the instance down on every exit path (success, error,
  cancel, timeout, killpill): `podman rm -af --force` (removes ALL containers in
  the instance, not just $WM_JOB_ID — conmon reparents so process-tree kill is
  insufficient) + kill the service + remove the dir.
- unshare (trusted) keeps using the shared service unchanged.

Verified e2e on native rootless podman + nsjail: a job that detaches an extra
`sleep 600` container completes successfully and the extra container is killed
when the job ends (no leak). Tradeoff: per-job storage re-pulls images (no shared
cache yet); a shared read-only image store is a follow-up optimization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:39:34 +00:00
Ruben FiszelandClaude Opus 4.8 d6171b7d79 docs: recommend rootless podman worker group over legacy dind
Reframe the dind sidecar as legacy in docker-compose.yml and the README, and
point to the recommended path: a dedicated worker group with the rootless podman
container runtime (CONTAINER_RUNTIME=podman / the Container runtime UI option),
which keeps `# docker` scripts unchanged while removing the privileged daemon and
the network-reachable socket. Add a commented windmill_worker_docker example
(full image, user 1000, /dev/fuse) with honest caveats: it runs podman inside the
worker container, so memory monitoring needs cgroup v2 delegation and the
strongest isolation comes from a dedicated host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:03:48 +00:00
Ruben FiszelandClaude Opus 4.8 dd9a983f42 feat(frontend): worker-group container runtime toggle
Add a "Container runtime" toggle to the worker group config drawer
(WorkerGroup.svelte) that sets container_runtime: "podman", enabling the
rootless podman runtime for docker-mode jobs. Mirrors the native_mode pattern.
Shows a warning when enabled but the group has no worker tags (so no docker job
could be routed to it). The tag stays independent — no hardcoded "docker" tag.

Verified in a real browser (system chromium): toggle renders between Native mode
and Alerts, is interactive, persists via ConfigService; npm run check:fast clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:01:40 +00:00
Ruben FiszelandClaude Opus 4.8 aeee148723 build(docker): ship rootless podman in the full images
Add podman + crun + uidmap + fuse-overlayfs + slirp4netns and a windmill user
(uid 1000) with a subuid/subgid range to DockerfileFull and DockerfileFullEe, so
the `container_runtime: podman` worker-group option works out of the box on the
*-full images. Run the docker worker group as `user: "1000:1000"` for a rootless
(unprivileged) daemon; root still works but is rootful.

Base/slim images are untouched (kept lean). Verified on debian:bookworm-slim:
packages resolve (podman 4.3.1), user + subuid set up, ~103MB layer delta
(rounding error on the multi-GB full image).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:51:22 +00:00
Ruben FiszelandClaude Opus 4.8 52f39a0e04 feat(worker): rootless podman container runtime for docker-mode jobs
Add a per-worker-group `container_runtime` option. When set to "podman", the
worker starts a rootless, daemonless `podman system service` on a unix socket
and points DOCKER_HOST at it, so docker-mode jobs (`# docker` bash scripts) run
against a Docker-API-compatible daemon without a privileged dind sidecar or the
host Docker socket. bollard's existing handle_docker_job talks to it unchanged
(validated: inspect/wait/logs/stats/kill/stop/remove against podman 5.8.2).

- windmill-common: container_runtime field on WorkerConfigOpt/WorkerConfig,
  loaded from group config or CONTAINER_RUNTIME env.
- windmill-worker: start_container_runtime_maybe() spawns the rootless podman
  service once per process (i_worker==1), waits for the socket, sets DOCKER_HOST.
  Respects an existing DOCKER_HOST (legacy dind) and degrades with a clear log if
  the podman binary is absent (use a *-full image or an init script).
- monitor: restart the worker (killpill) when container_runtime changes.
- api-configs: allow container_runtime in CE config updates.

Requires rootless podman with cgroup v2 delegation for memory monitoring;
podman ships in the *-full images (next commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:48:14 +00:00
Ruben FiszelandClaude Opus 4.8 956b07a095 fix(worker): hint how to enable docker when no daemon is reachable
With the dind sidecar now opt-in, a docker-mode bash script on a worker
with neither DOCKER_HOST nor a mounted host socket fails with a generic
"cannot connect to the Docker daemon" error. Prepend a Windmill-specific
hint to the job logs in that case, pointing at the --profile dind opt-in.
Only fires when no daemon is configured, so it stays quiet for valid
dind/host-socket setups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:43:55 +00:00
Ruben FiszelandClaude Opus 4.8 c2a133c169 docs(security): make dind opt-in and warn about untrusted use
The repo-root docker-compose.yml quickstart ran a privileged root Docker
daemon (dind) on tcp://dind:2375 with no TLS on the worker network, and
pointed workers at it via DOCKER_HOST. Because user scripts share the
worker network namespace (no --net isolation by design), any user who can
run a script could reach the daemon and escalate to root on the dind host.

This is a docs/deployment hardening change only — no worker sandbox or
network-isolation behavior is changed:

- Gate the dind sidecar behind the "dind" compose profile so it no longer
  starts by default; make DOCKER_HOST and the dind depends_on opt-in.
- Add a prominent SECURITY WARNING comment on the dind service and a
  warning callout in the README self-host section: trusted single-tenant
  use only, never untrusted/multi-tenant.
- Document how to enable dind TLS + client-cert auth (DOCKER_TLS_CERTDIR,
  tcp://dind:2376, cert volumes), including the caveat that TLS does not
  protect against a malicious script running on the worker itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:46:53 +00:00
15 changed files with 466 additions and 111 deletions
+38
View File
@@ -207,6 +207,44 @@ docker compose up -d
Go to http://localhost - default credentials: `admin@windmill.dev` / `changeme`
> [!NOTE]
> To run `# docker` scripts (bash scripts with the `# docker` annotation): just give a
> worker a `*-full` image (ships podman). `# docker` scripts are auto-tagged `docker`
> (served by default workers out of the box; route them to a dedicated/bigger group
> with `WORKER_TAGS=docker`). On a worker with **no Docker daemon provided** (no
> `DOCKER_HOST`, no mounted `/var/run/docker.sock`), Windmill runs each docker job in
> its own ephemeral **rootless podman**, torn down with the job — no privileged daemon,
> no host socket, scripts unchanged. (Docker jobs use disk like any other job — bound it
> at the infra level, e.g. a sized `/tmp/windmill` volume.) On old kernels (<5.13)
> also expose `/dev/fuse`. The `*-full` image is also the batteries-included runtime —
> on top of the base (TS/Bun/Deno, Python, Go) it adds Java, .NET, Ruby, R, Rust,
> Ansible, and Nushell (plus Oracle/Kerberos in EE). See the default `windmill_worker`
> notes in
> [docker-compose.yml](./docker-compose.yml). To use an external/host Docker daemon
> instead (legacy), provide `DOCKER_HOST` or mount `/var/run/docker.sock`.
>
> **Mixed fleets:** `docker` is a default tag, so a base/slim default worker (no podman)
> can pick up a docker job and fail it ("no Docker daemon… use a `*-full` image"). If not
> all your default workers use a `*-full` image, route docker jobs to a dedicated
> `*-full` group with `WORKER_TAGS=docker` rather than relying on the default tag.
>
> **Security:** keep the docker worker running as **root** (the default — don't set a
> non-root `user`): windmill drops the per-job podman to a non-root uid itself, so
> containers are rootless **and** the worker's own secrets in `/proc` (`DATABASE_URL`
> etc.) stay root-owned — unreadable by a `docker run --pid=host` container. What this
> does **not** do is confine the container's *filesystem* view: the daemon runs outside
> the job's nsjail sandbox, so a `# docker` script can still `docker run -v <worker path>`
> to reach **world-readable** host state: other concurrent job dirs' world-readable files,
> the dependency cache, and world-readable `/proc` (e.g. `-v /proc` → process `cmdline`s /
> args) — though **not** `0400` files like `/proc/<pid>/environ`, so the worker's env
> secrets stay protected. (Corollary: don't pass secrets as command-line args; windmill
> uses env, not args.) So treat docker-capable workers as a **trusted-tenant** capability
> (a big improvement over privileged dind, but not a full sandbox) and prefer
> dedicated/per-workspace docker workers on shared multi-tenant fleets.
> If you enable **nsjail** (it's off by default), per-job podman is **not** auto-provided
> for docker jobs — nsjail means "fully sandbox jobs" and the daemon runs outside the jail,
> so docker jobs then require an explicit `DOCKER_HOST` or a separate non-nsjail worker group.
**Using an external database**: Set `DATABASE_URL` in `.env` to point to your managed Postgres (AWS RDS, GCP Cloud SQL, Azure, Neon, etc.) and set db replicas to 0.
### Kubernetes (Helm charts)
+1
View File
@@ -15620,6 +15620,7 @@ dependencies = [
"itertools 0.14.0",
"jsonwebtoken 8.3.0",
"lazy_static",
"libc",
"libffi-sys",
"libloading",
"mappable-rc",
+14 -8
View File
@@ -63,10 +63,12 @@ async fn list_worker_groups(
authed: ApiAuthed,
Extension(db): Extension<DB>,
) -> error::JsonResult<Vec<Config>> {
let mut configs_raw =
sqlx::query_as!(Config, "SELECT name, config FROM config WHERE name LIKE 'worker__%'")
.fetch_all(&db)
.await?;
let mut configs_raw = sqlx::query_as!(
Config,
"SELECT name, config FROM config WHERE name LIKE 'worker__%'"
)
.fetch_all(&db)
.await?;
// Remove the 'worker__' prefix from all config names
for config in configs_raw.iter_mut() {
if let Some(name) = &config.name {
@@ -119,10 +121,14 @@ async fn get_config(
) -> error::JsonResult<Option<serde_json::Value>> {
require_devops_role(&db, &authed.email).await?;
let config = sqlx::query_as!(Config, "SELECT name, config FROM config WHERE name = $1", name)
.fetch_optional(&db)
.await?
.map(|c| c.config);
let config = sqlx::query_as!(
Config,
"SELECT name, config FROM config WHERE name = $1",
name
)
.fetch_optional(&db)
.await?
.map(|c| c.config);
Ok(Json(config))
}
@@ -894,6 +894,19 @@ async fn create_script_internal<'c>(
}
check_scopes(&authed, || format!("scripts:write:{}", ns.path))?;
// Auto-route Bash `# docker` scripts to the `docker` tag (part of DEFAULT_TAGS)
// when no explicit tag is set, so they can be directed to docker-capable / bigger
// workers. Done here — before no-op detection and the insert — so an unchanged
// redeploy is still correctly detected as a no-op. Tag-only mechanism mirroring
// the routing half of bunnative/nativets (the bash executor handles `# docker`
// at runtime, so no distinct ScriptLang is needed).
if ns.tag.as_deref().map_or(true, |t| t.is_empty())
&& ns.language == ScriptLang::Bash
&& windmill_common::worker::BashAnnotations::parse(&ns.content).docker
{
ns.tag = Some(windmill_common::worker::DOCKER_BASH_TAG.to_string());
}
guard_script_from_debounce_data(&ns).await?;
let codebase = ns.codebase.as_ref();
+13
View File
@@ -169,6 +169,10 @@ lazy_static::lazy_static! {
"python3".to_string(),
"go".to_string(),
"bash".to_string(),
// Bash `# docker` scripts are auto-tagged `docker` (see DOCKER_BASH_TAG /
// BashAnnotations) so they can be routed to docker-capable workers; in
// DEFAULT_TAGS so default workers serve them out of the box.
DOCKER_BASH_TAG.to_string(),
"powershell".to_string(),
"nativets".to_string(),
"mysql".to_string(),
@@ -859,6 +863,15 @@ pub struct BashAnnotations {
pub sandbox: bool,
}
/// Tag assigned to Bash scripts carrying the `# docker` annotation so docker
/// jobs can be routed to docker-capable / bigger workers. This is a tag-only
/// mechanism (no distinct ScriptLang — the bash executor handles the annotation
/// at runtime), mirroring the routing half of `bunnative`/`nativets`. It is part
/// of DEFAULT_TAGS so default workers serve it out of the box; derived at script
/// create (stored on `script.tag`) and at preview push when no explicit tag is
/// set. Callers gate on `language == Bash`.
pub const DOCKER_BASH_TAG: &str = "docker";
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SqlResultCollectionStrategy {
LastStatementAllRows,
+8
View File
@@ -5744,6 +5744,14 @@ async fn push_inner<'c, 'd>(
} else {
ScriptLang::Nativets.as_str()
}
} else if x == &ScriptLang::Bash
&& raw_code.as_deref().is_some_and(|c| {
windmill_common::worker::BashAnnotations::parse(c).docker
})
{
// Bash `# docker` previews/raw runs route to the `docker`
// tag (deployed scripts get it stored at create time).
windmill_common::worker::DOCKER_BASH_TAG
} else {
x.as_str()
};
+1
View File
@@ -115,6 +115,7 @@ pem = { workspace = true, optional = true }
rsa = { workspace = true, optional = true }
urlencoding.workspace = true
nix.workspace = true
libc = "0.2"
bytes.workspace = true
reqwest.workspace = true
reqwest-middleware.workspace = true
@@ -70,6 +70,9 @@ mount {
{TMP_MOUNT_BLOCK}
# Per-job rootless podman socket (docker jobs under nsjail); empty otherwise.
{DOCKER_SOCK_MOUNT}
mount {
src: "{JOB_DIR}/main.sh"
dst: "/tmp/main.sh"
+285 -18
View File
@@ -41,8 +41,8 @@ use crate::handle_child::run_future_with_polling_update_job_poller;
use crate::{
common::{
build_args_map, build_command_with_isolation, get_reserved_variables, read_file,
read_file_content, resolve_nsjail_timeout, resolve_nsjail_tmp_mount_block, start_child_process,
OccupancyMetrics, DEV_CONF_NSJAIL,
read_file_content, resolve_nsjail_timeout, resolve_nsjail_tmp_mount_block,
start_child_process, OccupancyMetrics, DEV_CONF_NSJAIL,
},
get_proxy_envs_for_lang,
handle_child::handle_child,
@@ -180,17 +180,6 @@ exit $exit_status
let _ = write_file(job_dir, "result.out", "")?;
let _ = write_file(job_dir, "result2.out", "")?;
// Forward DOCKER_HOST to the bash script when in docker mode so the docker CLI
// connects to the right daemon (e.g. a dind sidecar instead of /var/run/docker.sock)
let docker_envs: Vec<(&str, String)> = if annotation.docker {
["DOCKER_HOST", "DOCKER_TLS_VERIFY", "DOCKER_CERT_PATH"]
.iter()
.filter_map(|k| std::env::var(k).ok().map(|v| (*k, v)))
.collect()
} else {
vec![]
};
// Check if this is a regular job (not init or periodic script)
// Init/periodic scripts need full system access without isolation
let is_regular_job = job
@@ -203,6 +192,98 @@ exit $exit_status
// Use nsjail if globally enabled OR if script has #sandbox annotation
let nsjail = (is_sandboxing_enabled() || annotation.sandbox) && is_regular_job;
// Docker runtime selection: if a Docker daemon is already provided — DOCKER_HOST
// set, or the host socket mounted at /var/run/docker.sock — use it (legacy,
// backwards compatible). DOCKER_HOST counts in all modes (a tcp:// dind stays
// reachable through the jail since the sandbox doesn't isolate the network); a
// mounted /var/run/docker.sock only counts when NOT under nsjail (the bash nsjail
// proto never bind-mounts it into the jail, so it's unreachable there).
//
// Otherwise, for a `# docker` job, start a per-job rootless podman — BUT NOT under
// nsjail. The per-job daemon runs OUTSIDE the jail and a `# docker` script can
// bind-mount worker-visible paths through it, bypassing the jail's filesystem
// isolation. Auto-providing that under nsjail would be a surprising hole: enabling
// nsjail is an explicit "fully sandbox jobs" signal, so we refuse instead and make
// the operator opt into docker explicitly (a provided DOCKER_HOST, or a worker
// group without nsjail). Requires podman in the image (the *-full images).
#[cfg(feature = "dind")]
let docker_daemon_provided = std::env::var("DOCKER_HOST").is_ok()
|| (!nsjail && std::path::Path::new("/var/run/docker.sock").exists());
#[cfg(feature = "dind")]
let per_job_podman: Option<PerJobPodman> = if annotation.docker && !docker_daemon_provided {
if nsjail {
return Err(Error::ExecutionErr(
"`# docker` jobs are not given an automatic container runtime under nsjail \
sandboxing: the per-job podman would run outside the sandbox and a docker \
script could bind-mount worker-visible paths through it, weakening the \
isolation you enabled nsjail for. Run docker jobs on a worker group without \
nsjail, or provide a Docker daemon explicitly via DOCKER_HOST (a \
network-reachable daemon — a mounted unix socket is not reachable inside the \
jail)."
.to_string(),
));
}
Some(start_per_job_podman(job_dir).await?)
} else {
None
};
// DOCKER_HOST handed to the script's docker CLI when using per-job podman: under
// nsjail the per-job socket is bind-mounted into the jail at /tmp/podman.sock
// (via {DOCKER_SOCK_MOUNT}); otherwise the script reaches the host socket directly.
let docker_host_for_script: Option<String> = {
#[cfg(feature = "dind")]
{
per_job_podman.as_ref().map(|p| {
if nsjail {
"unix:///tmp/podman.sock".to_string()
} else {
p.docker_host.clone()
}
})
}
#[cfg(not(feature = "dind"))]
{
None
}
};
// Forward DOCKER_HOST to the bash script in docker mode: the per-job podman
// socket when one was started, else the provided DOCKER_HOST / docker socket.
let docker_envs: Vec<(&str, String)> = if annotation.docker {
if let Some(dh) = &docker_host_for_script {
vec![("DOCKER_HOST", dh.clone())]
} else {
["DOCKER_HOST", "DOCKER_TLS_VERIFY", "DOCKER_CERT_PATH"]
.iter()
.filter_map(|k| std::env::var(k).ok().map(|v| (*k, v)))
.collect()
}
} else {
vec![]
};
// nsjail mount block that exposes the per-job podman socket inside the jail.
let docker_sock_mount: String = {
#[cfg(feature = "dind")]
{
per_job_podman
.as_ref()
.map(|p| {
format!(
"mount {{\n src: \"{}\"\n dst: \"/tmp/podman.sock\"\n is_bind: true\n rw: true\n}}\n",
p.host_sock
)
})
.unwrap_or_default()
}
#[cfg(not(feature = "dind"))]
{
String::new()
}
};
let child = if nsjail {
let nsjail_timeout =
resolve_nsjail_timeout(conn, &job.workspace_id, job.id, job.timeout).await;
@@ -219,6 +300,7 @@ exit $exit_status
"{TMP_MOUNT_BLOCK}",
&resolve_nsjail_tmp_mount_block(job_dir).await,
)
.replace("{DOCKER_SOCK_MOUNT}", &docker_sock_mount)
.replace("{TIMEOUT}", &nsjail_timeout),
)?;
let mut cmd_args = vec![
@@ -319,8 +401,11 @@ exit $exit_status
worker_name,
occupancy_metrics,
_killpill_rx,
per_job_podman.as_ref().map(|p| p.docker_host.clone()),
)
.await;
// per_job_podman drops here (after handle_docker_job completes), tearing down
// the per-job podman instance so no container outlives the job.
}
let result_json_path = format!("{job_dir}/result.json");
@@ -369,8 +454,12 @@ async fn rm_container(client: &bollard::Docker, container_id: &str) {
#[cfg(feature = "dind")]
/// Connect to the Docker daemon, respecting DOCKER_HOST if set (e.g. for dind sidecar),
/// otherwise falling back to the default unix socket at /var/run/docker.sock.
fn connect_docker() -> Result<bollard::Docker, bollard::errors::Error> {
if std::env::var("DOCKER_HOST").is_ok() {
fn connect_docker(docker_host: Option<&str>) -> Result<bollard::Docker, bollard::errors::Error> {
if let Some(dh) = docker_host {
// Per-job rootless podman: connect to the job's own unix socket.
let path = dh.strip_prefix("unix://").unwrap_or(dh);
bollard::Docker::connect_with_unix(path, 120, bollard::API_DEFAULT_VERSION)
} else if std::env::var("DOCKER_HOST").is_ok() {
// DOCKER_HOST is set — use it (e.g. tcp://dind:2375 for docker-in-docker)
bollard::Docker::connect_with_defaults()
} else {
@@ -379,6 +468,180 @@ fn connect_docker() -> Result<bollard::Docker, bollard::errors::Error> {
}
}
// A per-job rootless podman instance used for docker jobs (any sandbox mode —
// nsjail, unshare, or none) when no Docker daemon is otherwise provided.
// The job's docker CLI and handle_docker_job (in the worker) both talk to this
// one instance's socket; because every container the job can create is registered
// in this instance's isolated storage, tearing it down (`podman system reset`,
// which also removes images and the subuid-owned overlay layers — see Drop)
// guarantees no container outlives the job — even detached or extra ones.
#[cfg(feature = "dind")]
struct PerJobPodman {
dir: String,
host_sock: String,
docker_host: String,
service: Option<std::process::Child>,
}
// Blocking teardown of a per-job podman instance: `system reset` removes ALL
// containers AND images in the per-job store and, crucially, deletes the
// subuid-owned overlay layers a plain `rm -rf` (or `rm -af`, which only touches
// containers) cannot — preventing a storage leak. Wrapped in `timeout` so a wedged
// runtime/storage can't hang the caller indefinitely.
#[cfg(feature = "dind")]
fn teardown_per_job_podman(dir: String, service: Option<std::process::Child>) {
let storage = format!("{dir}/storage");
let runroot = format!("{dir}/runroot");
let _ = std::process::Command::new("timeout")
.args([
"30",
"podman",
"--root",
&storage,
"--runroot",
&runroot,
"system",
"reset",
"--force",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
if let Some(mut child) = service {
// Kill the whole process group: when the worker is root the daemon runs as a
// `runuser` grandchild, so killing only the direct child would leak it.
unsafe { libc::kill(-(child.id() as i32), libc::SIGKILL) };
let _ = child.kill();
let _ = child.wait();
}
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(feature = "dind")]
impl Drop for PerJobPodman {
fn drop(&mut self) {
let dir = std::mem::take(&mut self.dir);
let service = self.service.take();
// Teardown shells out (`podman system reset`) and can do non-trivial I/O, so
// offload it to the blocking pool rather than stalling the Tokio worker thread
// this Drop runs on. If we're not on a runtime (e.g. tests), run inline.
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
handle.spawn_blocking(move || teardown_per_job_podman(dir, service));
}
Err(_) => teardown_per_job_podman(dir, service),
}
}
}
// Start a per-job rootless podman service in <job_dir>/podman with isolated
// storage, returning the host socket path. Inherits the system container config
// (/etc/containers) but overrides storage and the network backend via a per-job
// user config so both are job-scoped (and rootful podman elsewhere is untouched).
//
// SECURITY: this is only ever started in NON-nsjail modes (unshare/none) — caller
// refuses to auto-provide it under nsjail, since the daemon runs outside any sandbox.
// A `# docker` script controls this daemon over the Docker API and can bind-mount any
// WORLD-READABLE host path (e.g. `docker run -v /tmp/windmill/...` for other job dirs /
// caches, or `-v /proc` for process cmdlines/args) — so it is NOT a filesystem sandbox,
// and docker-capable workers stay a TRUSTED-TENANT capability (prefer dedicated/per-
// workspace docker workers on shared fleets). What IS protected, when the worker runs
// as root (the default) and the daemon is dropped to a non-root uid here: `0400` files
// like the worker's `/proc/<worker>/environ` (DATABASE_URL etc.) stay root-owned and
// unreadable by the uid-1000 container even via `-v /proc` or `--pid=host` (so don't
// pass secrets as command-line args), and a container escape is unprivileged.
#[cfg(feature = "dind")]
async fn start_per_job_podman(job_dir: &str) -> Result<PerJobPodman, Error> {
use std::os::unix::process::CommandExt;
let dir = format!("{job_dir}/podman");
let xdg = format!("{dir}/xdg");
tokio::fs::create_dir_all(&xdg).await.map_err(to_anyhow)?;
// Default this rootless podman's container networking to slirp4netns: the
// netavark bridge default fails rootless on hosts without the required
// nftables setup. Scope it to THIS instance via a user (`$HOME`) config so we
// don't touch the global `/etc/containers` config (which would also change
// rootful podman behavior for anyone else in the image). `$HOME` is the job
// dir below for both the root-drop and non-root launch paths.
let containers_conf_dir = format!("{dir}/.config/containers");
tokio::fs::create_dir_all(&containers_conf_dir)
.await
.map_err(to_anyhow)?;
tokio::fs::write(
format!("{containers_conf_dir}/containers.conf"),
"[containers]\nnetns = \"slirp4netns\"\n",
)
.await
.map_err(to_anyhow)?;
let host_sock = format!("{dir}/podman.sock");
let _ = tokio::fs::remove_file(&host_sock).await;
let storage = format!("{dir}/storage");
let runroot = format!("{dir}/runroot");
let sock_url = format!("unix://{host_sock}");
// If the worker is root (the default for docker workers), drop the per-job podman
// to a non-root uid via `runuser` so it runs ROOTLESS. This keeps the worker's
// secrets in `/proc/<worker>/environ` (DATABASE_URL etc.) root-owned and therefore
// unreadable by the uid-1000 container even via `docker run --pid=host`, and makes
// a container escape land as an unprivileged user. NB: it does NOT confine the
// daemon's filesystem view — a script can still `docker run -v <worker path>` to
// reach other job dirs / caches, so docker-capable workers remain trusted-tenant.
// A non-root worker can't drop further (it runs podman rootless as itself, and a
// `--pid=host` container shares its uid) — run the docker worker as root for the
// /proc env protection. `process_group(0)` lets teardown kill the whole tree (with
// `runuser`, podman is a grandchild, not the direct child).
let mut cmd = if unsafe { libc::geteuid() } == 0 {
let launch = format!(
"set -e\nchown -R 1000:1000 '{dir}'\nexec runuser -u \"$(id -nu 1000)\" -- env \
HOME='{dir}' XDG_RUNTIME_DIR='{xdg}' podman --root '{storage}' --runroot '{runroot}' \
system service --time=0 '{sock_url}'\n"
);
let mut c = std::process::Command::new("bash");
c.args(["-c", &launch]);
c
} else {
let mut c = std::process::Command::new("podman");
// HOME=<job dir> so podman picks up the per-job containers.conf written
// above (netns override) and keeps its config/cache job-scoped.
c.env("HOME", &dir);
c.args([
"--root",
&storage,
"--runroot",
&runroot,
"system",
"service",
"--time=0",
&sock_url,
]);
c
};
let service = cmd
.process_group(0)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| {
Error::ExecutionErr(format!(
"no Docker daemon provided (DOCKER_HOST/socket) and failed to start the per-job \
podman runtime: {e}. Use a windmill *-full image (ships podman), or provide a \
Docker daemon via DOCKER_HOST or a mounted /var/run/docker.sock."
))
})?;
// Wait (up to ~10s) for the rootless podman service socket to appear.
for _ in 0..50 {
if tokio::fs::metadata(&host_sock).await.is_ok() {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
Ok(PerJobPodman {
dir,
docker_host: format!("unix://{host_sock}"),
host_sock,
service: Some(service),
})
}
#[cfg(feature = "dind")]
async fn handle_docker_job(
job_id: Uuid,
@@ -390,10 +653,13 @@ async fn handle_docker_job(
worker_name: &str,
occupancy_metrics: &mut OccupancyMetrics,
killpill_rx: &mut tokio::sync::broadcast::Receiver<()>,
// Some(socket) when a per-job rootless podman runs the container;
// None to use the provided DOCKER_HOST / default socket (legacy).
docker_host: Option<String>,
) -> Result<Box<RawValue>, Error> {
use crate::job_logger::append_logs_with_compaction;
let client = connect_docker().map_err(to_anyhow)?;
let client = connect_docker(docker_host.as_deref()).map_err(to_anyhow)?;
let container_id = job_id.to_string();
let inspected = client.inspect_container(&container_id, None).await;
@@ -437,8 +703,9 @@ async fn handle_docker_job(
let (tx, mut rx) = tokio::sync::broadcast::channel::<()>(1);
let workspace_id2 = workspace_id.to_string();
let mut killpill_rx = killpill_rx.resubscribe();
let docker_host_logs = docker_host.clone();
let logs = tokio::spawn(async move {
let client = connect_docker().map_err(to_anyhow);
let client = connect_docker(docker_host_logs.as_deref()).map_err(to_anyhow);
if let Ok(client) = client {
let mut log_stream = client.logs(
&ncontainer_id,
@@ -506,7 +773,7 @@ async fn handle_docker_job(
}
});
let mem_client = connect_docker().map_err(to_anyhow);
let mem_client = connect_docker(docker_host.as_deref()).map_err(to_anyhow);
let ncontainer_id = container_id.clone();
let result = run_future_with_polling_update_job_poller(
job_id,
+32 -32
View File
@@ -49,27 +49,40 @@ services:
logging: *default-logging
# Docker-in-Docker sidecar: provides an isolated Docker daemon so user scripts
# can run containers without accessing the host Docker socket.
dind:
image: docker:dind
privileged: true
restart: unless-stopped
environment:
DOCKER_TLS_CERTDIR: ""
volumes:
- dind-data:/var/lib/docker
expose:
- 2375
healthcheck:
test: ["CMD", "docker", "info"]
interval: 10s
timeout: 5s
retries: 5
logging: *default-logging
windmill_worker:
image: ${WM_IMAGE}
# --- To run `# docker` scripts (bash scripts with a `# docker` annotation) on
# this worker: comment the `image` line above and uncomment the *-full
# image below. Each docker job then runs in its OWN ephemeral rootless
# podman, torn down with the job — no dind sidecar, no host Docker socket,
# your scripts unchanged. The *-full image also bundles the heavier runtimes
# (Java, .NET, Ruby, R, Rust, Ansible, Nushell). `# docker` scripts are
# auto-tagged `docker` (served by default workers); to send them to a
# dedicated/bigger group instead, run a worker with WORKER_TAGS=docker. ---
# image: ghcr.io/windmill-labs/windmill-full:main # windmill-ee-full:main for EE
# On old kernels (<5.13, no native rootless overlay) podman needs fuse-overlayfs;
# expose the device then (harmless to keep; auto-provided by `privileged` if the
# host has it):
# devices:
# - /dev/fuse
# Keep this worker running as ROOT (the default — do NOT set `user: 1000`):
# windmill itself drops the per-job podman to a non-root uid, so containers are
# rootless AND the worker's own secrets in /proc (DATABASE_URL etc.) stay
# root-owned, unreadable by a `docker run --pid=host` container. (NB: this does
# not confine the container's filesystem view — a `# docker` script can still
# `docker run -v` to read WORLD-READABLE host state: other job dirs' world-readable
# files, the dep cache, and world-readable /proc like process cmdlines/args — though
# NOT 0400 files like /proc/<pid>/environ (env secrets stay safe; don't pass secrets
# as args). So treat docker workers as trusted-tenant / dedicate them on shared
# fleets.) Docker jobs use disk like any other job (no docker-specific cap); bound
# it at the infra level (e.g. a sized volume for /tmp/windmill) for all jobs.
# If you enable nsjail (DISABLE_NSJAIL=false or job isolation = nsjail), per-job
# podman is NOT auto-provided for docker jobs — nsjail signals "fully sandbox jobs"
# and the podman daemon runs outside the jail, so docker jobs then require an
# explicit DOCKER_HOST or a separate non-nsjail worker group. (nsjail is off by
# default; the default worker uses unshare, so docker works out of the box here.)
# To use an external/host Docker daemon instead (legacy): set DOCKER_HOST or mount
# /var/run/docker.sock.
pull_policy: always
deploy:
replicas: 3
@@ -86,26 +99,14 @@ services:
- DATABASE_URL=${DATABASE_URL}
- MODE=worker
- WORKER_GROUP=default
# If running with non-root/non-windmill UID (e.g., user: "1001:1001"),
# add: - HOME=/tmp
- FAVOR_UNSHARE_PID=true
# Connect to the dind sidecar instead of the host Docker socket
- DOCKER_HOST=tcp://dind:2375
depends_on:
db:
condition: service_healthy
dind:
condition: service_healthy
# to mount the worker folder to debug, KEEP_JOB_DIR=true and mount /tmp/windmill
volumes:
- worker_dependency_cache:/tmp/windmill/cache
- worker_logs:/tmp/windmill/logs
## WARNING: mounting the host Docker socket grants user scripts full access to
## the host Docker daemon, enabling host filesystem access and privilege escalation.
## Only use this if you fully trust all users who can run scripts.
## To use it, remove the DOCKER_HOST env var and dind depends_on above,
## and uncomment the line below:
# - /var/run/docker.sock:/var/run/docker.sock
logging: *default-logging
@@ -237,4 +238,3 @@ volumes:
windmill_index: null
lsp_cache: null
caddy_data: null
dind-data: null
+29
View File
@@ -31,6 +31,35 @@ RUN apt-get install -y ruby ruby-bundler
RUN apt-get install -y r-base-dev \
&& Rscript -e 'install.packages("renv", lib="/usr/lib/R/library", repos="https://cloud.r-project.org")'
# Rootless container runtime (podman) for docker-mode jobs. When a `# docker` job
# runs on a worker with no Docker daemon provided (no DOCKER_HOST, no mounted
# /var/run/docker.sock), Windmill automatically starts a per-job rootless podman —
# no privileged dind sidecar or host Docker socket. Run the docker worker group as
# `user: "1000:1000"` for a rootless (unprivileged) daemon; running as root still
# works but is rootful (less isolated).
RUN apt-get -y update \
&& apt-get install -y --no-install-recommends \
podman \
uidmap \
fuse-overlayfs \
slirp4netns \
crun \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -u 1000 -m -s /bin/bash windmill 2>/dev/null || true
# Ensure a subuid/subgid range exists for rootless podman (useradd usually adds
# one already; only append if it didn't, to avoid a duplicate range).
RUN grep -q '^windmill:' /etc/subuid || echo "windmill:100000:65536" >> /etc/subuid; \
grep -q '^windmill:' /etc/subgid || echo "windmill:100000:65536" >> /etc/subgid
# newuidmap/newgidmap need privilege to map the subuid range for rootless podman;
# ensure they are setuid in case the package's file capabilities are lost in the
# image layers.
RUN chmod u+s /usr/bin/newuidmap /usr/bin/newgidmap
# NB: rootless container networking is defaulted to slirp4netns (the netavark
# bridge default fails rootless without nftables setup) per-job by the worker via
# a job-scoped containers.conf, rather than globally here, so rootful podman is
# unaffected.
# Fix UV cache permissions for non-root user support (uid 1000, etc.)
# The uv tool install ansible command populates the UV cache with root-owned files
RUN chmod -R a+rw /tmp/windmill/cache/uv && \
+29
View File
@@ -61,6 +61,35 @@ RUN apt-get install -y iptables
# Kerberos runtime
RUN apt-get install -y libsasl2-modules-gssapi-mit krb5-user
# Rootless container runtime (podman) for docker-mode jobs. When a `# docker` job
# runs on a worker with no Docker daemon provided (no DOCKER_HOST, no mounted
# /var/run/docker.sock), Windmill automatically starts a per-job rootless podman —
# no privileged dind sidecar or host Docker socket. Run the docker worker group as
# `user: "1000:1000"` for a rootless (unprivileged) daemon; running as root still
# works but is rootful (less isolated).
RUN apt-get -y update \
&& apt-get install -y --no-install-recommends \
podman \
uidmap \
fuse-overlayfs \
slirp4netns \
crun \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -u 1000 -m -s /bin/bash windmill 2>/dev/null || true
# Ensure a subuid/subgid range exists for rootless podman (useradd usually adds
# one already; only append if it didn't, to avoid a duplicate range).
RUN grep -q '^windmill:' /etc/subuid || echo "windmill:100000:65536" >> /etc/subuid; \
grep -q '^windmill:' /etc/subgid || echo "windmill:100000:65536" >> /etc/subgid
# newuidmap/newgidmap need privilege to map the subuid range for rootless podman;
# ensure they are setuid in case the package's file capabilities are lost in the
# image layers.
RUN chmod u+s /usr/bin/newuidmap /usr/bin/newgidmap
# NB: rootless container networking is defaulted to slirp4netns (the netavark
# bridge default fails rootless without nftables setup) per-job by the worker via
# a job-scoped containers.conf, rather than globally here, so rootful podman is
# unaffected.
# Fix UV cache permissions for non-root user support (uid 1000, etc.)
# The uv tool install ansible command populates the UV cache with root-owned files
RUN chmod -R a+rw /tmp/windmill/cache/uv && \
@@ -1007,21 +1007,6 @@
function onScriptLanguageTrigger(lang: 'docker' | 'bunnative' | ScriptLang) {
if (lang == 'docker') {
if (isCloudHosted()) {
sendUserToast(
'You cannot use Docker scripts on the multi-tenant platform. Use a dedicated instance or self-host windmill instead.',
true,
[
{
label: 'Learn more',
callback: () => {
window.open('https://www.windmill.dev/docs/advanced/docker', '_blank')
}
}
]
)
return
}
template = 'docker'
} else if (lang == 'bunnative') {
template = 'bunnative'
@@ -7,8 +7,6 @@
import FlowScriptPicker from '../pickers/FlowScriptPicker.svelte'
import PickHubScript from '../pickers/PickHubScript.svelte'
import WorkspaceScriptPicker from '../pickers/WorkspaceScriptPicker.svelte'
import { isCloudHosted } from '$lib/cloud'
import { sendUserToast } from '$lib/toast'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import { Check, Code, Zap } from 'lucide-svelte'
@@ -259,23 +257,6 @@
{label}
lang={lang == 'docker' ? 'bash' : lang}
on:click={() => {
if (lang == 'docker') {
if (isCloudHosted()) {
sendUserToast(
'You cannot use Docker scripts on the multi-tenant platform. Use a dedicated instance or self-host windmill instead.',
true,
[
{
label: 'Learn more',
callback: () => {
window.open('https://www.windmill.dev/docs/advanced/docker', '_blank')
}
}
]
)
return
}
}
dispatch('new', {
language: lang == 'docker' ? 'bash' : lang,
kind,
@@ -4,7 +4,6 @@
</script>
<script lang="ts">
import { isCloudHosted } from '$lib/cloud'
import { sendUserToast } from '$lib/toast'
import FlowScriptPickerQuick from '../pickers/FlowScriptPickerQuick.svelte'
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
@@ -401,24 +400,6 @@
{label}
lang={lang == 'docker' ? 'bash' : lang}
on:click={() => {
if (lang == 'docker') {
if (isCloudHosted()) {
sendUserToast(
'You cannot use Docker scripts on the multi-tenant platform. Use a dedicated instance or self-host windmill instead.',
true,
[
{
label: 'Learn more',
callback: () => {
window.open('https://www.windmill.dev/docs/advanced/docker', '_blank')
}
}
]
)
return
}
}
dispatch('new', {
kind: selectedKind,
inlineScript: {