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>
This commit is contained in:
Ruben Fiszel
2026-06-04 14:45:13 +00:00
co-authored by Claude Opus 4.8
parent 40a387a17b
commit cccd8b9847
7 changed files with 30 additions and 103 deletions
+8 -8
View File
@@ -208,14 +208,14 @@ 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), add a
> dedicated worker group with the rootless **podman** container runtime: each docker
> job runs in its own ephemeral rootless podman, torn down with the job — no
> privileged daemon and no host Docker socket, with your scripts unchanged. Use a
> `*-full` image (ships podman), set `CONTAINER_RUNTIME=podman`, and tag your docker
> scripts to route them to that group. See the commented `windmill_worker_docker`
> service in [docker-compose.yml](./docker-compose.yml), or the **Container runtime**
> toggle on a worker group in the UI (Workers → worker group config).
> To run `# docker` scripts (bash scripts with the `# docker` annotation): on a worker
> with **no Docker daemon provided** (no `DOCKER_HOST`, no mounted `/var/run/docker.sock`),
> Windmill automatically runs each docker job in its own ephemeral **rootless podman**,
> torn down with the job — no privileged daemon, no host socket, scripts unchanged. Just
> use a `*-full` image (ships podman) and tag your docker scripts to a dedicated worker
> group; see the commented `windmill_worker_docker` service 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`.
**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.
-3
View File
@@ -315,9 +315,6 @@ pub async fn initial_load(
additional_python_paths: None,
pip_local_dependencies: None,
native_mode,
container_runtime: std::env::var("CONTAINER_RUNTIME")
.ok()
.filter(|x| !x.is_empty()),
}));
}
}
+2 -4
View File
@@ -143,14 +143,12 @@ async fn update_config(
#[cfg(not(feature = "enterprise"))]
let config = if name.starts_with("worker__") {
// In CE, only allow setting worker_tags, cache_clear, init_bash, native_mode,
// and container_runtime
// In CE, only allow setting worker_tags, cache_clear, init_bash, and native_mode
serde_json::json!({
"worker_tags": config.get("worker_tags"),
"cache_clear": config.get("cache_clear"),
"init_bash": config.get("init_bash"),
"native_mode": config.get("native_mode"),
"container_runtime": config.get("container_runtime")
"native_mode": config.get("native_mode")
})
} else {
config
+2 -14
View File
@@ -234,7 +234,6 @@ lazy_static::lazy_static! {
pip_local_dependencies: Default::default(),
env_vars: Default::default(),
native_mode: false,
container_runtime: None,
});
pub static ref WORKER_PULL_QUERIES: arc_swap::ArcSwap<Vec<String>> = arc_swap::ArcSwap::from_pointee(vec![]);
@@ -1961,10 +1960,6 @@ pub async fn load_worker_config(
.or_else(|| load_additional_python_paths_from_env()),
env_vars: resolved_env_vars,
native_mode,
container_runtime: config
.container_runtime
.or_else(|| std::env::var("CONTAINER_RUNTIME").ok())
.and_then(|x| if x.is_empty() { None } else { Some(x) }),
})
}
@@ -2054,11 +2049,6 @@ pub struct WorkerConfigOpt {
pub env_vars_static: Option<HashMap<String, String>>,
pub env_vars_allowlist: Option<Vec<String>>,
pub native_mode: Option<bool>,
/// Container runtime for docker-mode jobs on this worker group. When set to
/// "podman", the worker starts a rootless podman service and points
/// DOCKER_HOST at it (see start_container_runtime). None = no managed runtime
/// (legacy dind/host-socket via DOCKER_HOST/socket still works if present).
pub container_runtime: Option<String>,
}
impl Default for WorkerConfigOpt {
@@ -2077,7 +2067,6 @@ impl Default for WorkerConfigOpt {
env_vars_static: Default::default(),
env_vars_allowlist: Default::default(),
native_mode: Default::default(),
container_runtime: Default::default(),
}
}
}
@@ -2096,13 +2085,12 @@ pub struct WorkerConfig {
pub pip_local_dependencies: Option<Vec<String>>,
pub env_vars: HashMap<String, String>,
pub native_mode: bool,
pub container_runtime: Option<String>,
}
impl std::fmt::Debug for WorkerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "WorkerConfig {{ worker_tags: {:?}, priority_tags_sorted: {:?}, dedicated_worker: {:?}, dedicated_workers: {:?}, init_bash: {:?}, periodic_script_bash: {:?}, periodic_script_interval_seconds: {:?}, cache_clear: {:?}, additional_python_paths: {:?}, pip_local_dependencies: {:?}, env_vars: {:?}, native_mode: {:?}, container_runtime: {:?} }}",
self.worker_tags, self.priority_tags_sorted, self.dedicated_worker, self.dedicated_workers, self.init_bash, self.periodic_script_bash, self.periodic_script_interval_seconds, self.cache_clear, self.additional_python_paths, self.pip_local_dependencies, self.env_vars.iter().map(|(k, v)| format!("{}: {}{} ({} chars)", k, &v[..3.min(v.len())], "***", v.len())).collect::<Vec<String>>().join(", "), self.native_mode, self.container_runtime)
write!(f, "WorkerConfig {{ worker_tags: {:?}, priority_tags_sorted: {:?}, dedicated_worker: {:?}, dedicated_workers: {:?}, init_bash: {:?}, periodic_script_bash: {:?}, periodic_script_interval_seconds: {:?}, cache_clear: {:?}, additional_python_paths: {:?}, pip_local_dependencies: {:?}, env_vars: {:?}, native_mode: {:?} }}",
self.worker_tags, self.priority_tags_sorted, self.dedicated_worker, self.dedicated_workers, self.init_bash, self.periodic_script_bash, self.periodic_script_interval_seconds, self.cache_clear, self.additional_python_paths, self.pip_local_dependencies, self.env_vars.iter().map(|(k, v)| format!("{}: {}{} ({} chars)", k, &v[..3.min(v.len())], "***", v.len())).collect::<Vec<String>>().join(", "), self.native_mode)
}
}
+15 -37
View File
@@ -96,27 +96,6 @@ pub async fn handle_bash_job(
let mut logs1 = "\n\n--- BASH CODE EXECUTION ---\n".to_string();
if annotation.docker {
logs1.push_str("docker mode\n");
// If neither DOCKER_HOST nor the host socket is available, the docker CLI
// in the script would fail with a generic "cannot connect to the Docker
// daemon" error. Surface a Windmill-specific hint instead. Skipped when
// container_runtime=podman is set: a per-job rootless podman is started
// below and provides the daemon.
if std::env::var("DOCKER_HOST").is_err()
&& !std::path::Path::new("/var/run/docker.sock").exists()
&& windmill_common::worker::WORKER_CONFIG
.load()
.container_runtime
.as_deref()
!= Some("podman")
{
logs1.push_str(
"WARNING: docker mode is set but no Docker daemon is reachable from this worker \
(DOCKER_HOST is unset and /var/run/docker.sock is not mounted). Give this worker \
Docker access: with docker-compose, enable the dind sidecar (`docker compose \
--profile dind up -d` and uncomment DOCKER_HOST in windmill_worker); with the Helm \
chart, set `exposeHostDocker: true`; otherwise set DOCKER_HOST or mount a Docker socket.\n",
);
}
}
if annotation.sandbox {
logs1.push_str("sandbox mode (nsjail)\n");
@@ -214,20 +193,18 @@ exit $exit_status
// Use nsjail if globally enabled OR if script has #sandbox annotation
let nsjail = (is_sandboxing_enabled() || annotation.sandbox) && is_regular_job;
// Every docker job gets its OWN ephemeral rootless podman instance (started here,
// torn down when this guard drops at the end of the function) so no container it
// spawns can outlive the job, in any sandbox mode. Gated strictly on
// container_runtime=podman: legacy docker access (an externally-provided
// DOCKER_HOST or a mounted /var/run/docker.sock, without container_runtime=podman)
// is left exactly as before.
// 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 (backwards
// compatible, unchanged). Otherwise start a per-job rootless podman instance for
// this docker job (its own ephemeral daemon, torn down when this guard drops at
// the end of the function, so no container it spawns can outlive the job, in any
// sandbox mode). Requires podman in the image (the *-full images) — else
// start_per_job_podman returns a clear error.
#[cfg(feature = "dind")]
let per_job_podman: Option<PerJobPodman> = if annotation.docker
&& windmill_common::worker::WORKER_CONFIG
.load()
.container_runtime
.as_deref()
== Some("podman")
{
let docker_daemon_provided = std::env::var("DOCKER_HOST").is_ok()
|| std::path::Path::new("/var/run/docker.sock").exists();
#[cfg(feature = "dind")]
let per_job_podman: Option<PerJobPodman> = if annotation.docker && !docker_daemon_provided {
Some(start_per_job_podman(job_dir).await?)
} else {
None
@@ -254,7 +231,7 @@ exit $exit_status
};
// Forward DOCKER_HOST to the bash script in docker mode: the per-job podman
// socket when container_runtime=podman, else the legacy DOCKER_HOST / docker socket.
// 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())]
@@ -543,8 +520,9 @@ async fn start_per_job_podman(job_dir: &str) -> Result<PerJobPodman, Error> {
.spawn()
.map_err(|e| {
Error::ExecutionErr(format!(
"container_runtime=podman but failed to start the per-job podman service: {e}. \
Use a windmill *-full image (ships podman)."
"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.
+3 -1
View File
@@ -106,7 +106,9 @@ services:
# - MODE=worker
# - WORKER_GROUP=docker
# - WORKER_TAGS=docker # only serve jobs tagged "docker" (set that tag on your docker scripts)
# - CONTAINER_RUNTIME=podman
# # No DOCKER_HOST / docker socket is provided, so docker jobs automatically use
# # the bundled rootless podman (per job). Provide a DOCKER_HOST or mount
# # /var/run/docker.sock instead to use an external/host Docker daemon (legacy).
# depends_on:
# db:
# condition: service_healthy
@@ -100,7 +100,6 @@
min_alive_workers_alert_threshold?: number
autoscaling?: AutoscalingConfig
native_mode?: boolean
container_runtime?: string
} = $state({})
function loadNConfig() {
@@ -206,7 +205,6 @@
periodic_script_bash?: string
periodic_script_interval_seconds?: number
native_mode?: boolean
container_runtime?: string
}
activeWorkers: number
customTags: string[] | undefined
@@ -643,40 +641,6 @@
</Label>
{/if}
{#if nconfig !== undefined}
<div class="mt-8"></div>
<Label label="Container runtime">
{#snippet header()}
<Tooltip>
{#snippet text()}
When set, docker-mode jobs (bash scripts with the "# docker" annotation)
run against a rootless podman daemon started by the worker, instead of a
privileged docker-in-docker sidecar or the host Docker socket. Requires a
worker image that ships podman (the *-full images). Run the worker group as
a non-root user for an unprivileged (rootless) runtime.
{/snippet}
</Tooltip>
{/snippet}
<Toggle
size="sm"
options={{ right: 'Enable container runtime (rootless podman)' }}
checked={nconfig?.container_runtime === 'podman'}
on:change={(ev) => {
if (nconfig !== undefined) {
nconfig.container_runtime = ev.detail ? 'podman' : undefined
}
}}
disabled={!canEditConfig}
/>
{#if nconfig?.container_runtime === 'podman' && (nconfig?.worker_tags == undefined || nconfig.worker_tags.length === 0)}
<Alert size="xs" type="warning" title="No tags on this worker group">
The container runtime is enabled but this worker group has no tags, so no
docker job can be routed to it. Add the tag(s) your docker scripts use.
</Alert>
{/if}
</Label>
{/if}
{#if nconfig !== undefined}
<div class="mt-8"></div>
<Label