Skip to content

[wrangler/miniflare] Add privileged_containers / --privileged-containers for FUSE in local dev#13748

Draft
Ben2W wants to merge 1 commit into
cloudflare:mainfrom
Ben2W:fuse-allow-privileged
Draft

[wrangler/miniflare] Add privileged_containers / --privileged-containers for FUSE in local dev#13748
Ben2W wants to merge 1 commit into
cloudflare:mainfrom
Ben2W:fuse-allow-privileged

Conversation

@Ben2W

@Ben2W Ben2W commented Apr 29, 2026

Copy link
Copy Markdown

Pairs with cloudflare/workerd#6596. Resolves cloudflare/workerd#5609.

Adds an opt-in dev config flag for FUSE in wrangler dev:

  • dev.privileged_containers in wrangler.json
  • --privileged-containers on the CLI

When set, miniflare asks workerd (via the per-DO container.allowPrivileged field added in the paired PR) to launch local containers with CAP_SYS_ADMIN, /dev/fuse, and AppArmor unconfined. This gives local containers permission to mount FUSE volumes.

Reproduction harness: https://github.com/Ben2W/workerd-fuse-local-repro
Docs PR: cloudflare/cloudflare-docs#30462

CC @emily-shen


@changeset-bot

changeset-bot Bot commented Apr 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 37d5ae5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
wrangler Minor
miniflare Minor
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch
@cloudflare/wrangler-bundler Patch
@cloudflare/pages-shared Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Ben2W added a commit to Ben2W/workerd that referenced this pull request Apr 29, 2026
…tainers

Containers using FUSE work in production but break in `wrangler dev`
because workerd doesn't grant CAP_SYS_ADMIN, mount /dev/fuse, or relax
AppArmor on the container it creates. Without those three knobs the
mount("fuse", ...) syscall inside the container fails.

Add a new boolean to ContainerOptions in workerd.capnp, off by default:

  struct ContainerOptions {
    imageName       @0 :Text;
    allowPrivileged @1 :Bool = false;  # NEW
  }

When set on a DurableObjectNamespace's container config, ContainerClient
adds the minimum HostConfig fields needed for FUSE to the
POST /containers/create payload:

  - CapAdd: ["SYS_ADMIN"]                 (for the mount() syscall)
  - Devices: [/dev/fuse]                  (so the kernel has something to open)
  - SecurityOpt: ["apparmor:unconfined"]  (bypasses the default docker
                                           apparmor profile's mount block)

No Privileged=true. Mirrors what Cloudflare's production container
runtime effectively provides for FUSE workers, without granting all
Linux capabilities or full /dev passthrough.

server.c++ reads the flag from the per-DO ContainerOptions config and
threads it through to the ContainerClient constructor. Off by default
means existing configs are unaffected. Only the local Docker code path
is touched; production is unaffected.

Also adds missing $Json.name annotations to docker_api::DeviceMapping
fields in docker-api.capnp so they serialize as PathOnHost /
PathInContainer / CgroupPermissions to match the Docker API. This is
load-bearing for the Devices bind above — without it, the entry
serializes as camelCase and Docker silently ignores it. Before this PR
no call site populated Devices, so the missing annotations were latent.

Follows the existing pattern of createSidecarContainer() which already
sets CapAdd=[NET_ADMIN] for its own need at container-client.c++:1958.

The matching workers-sdk PR
(cloudflare/workers-sdk#13748) adds
`dev.enable_containers_privileged_mode` and
`--enable-containers-privileged-mode` so wrangler / vite-plugin users
can opt in.

End-to-end reproduction (libc mount("fuse", ...) syscall succeeding
inside the container with the flag set, and failing without it) at
https://github.com/Ben2W/workerd-fuse-local-repro

Signed-off-by: Ben Werner <bewerner23@gmail.com>
*
* @default false
*/
enable_containers_privileged_mode: boolean;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name is pretty verbose. What about just privileged, enabled with wrangler dev --privileged?

Docs (and help text) will indicate what "privileged" means.

I defer to the @cloudflare/wrangler team on this, but that's my 2c.

If just privileged is too terse, then privileged_containers maybe (wrangler dev --privileged-containers).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout, --privileged-containers is 100% better than --enable-containers-privileged-mode. I don't have a strong opinion between --privileged and --privileged-containers, but --privileged-containers seems safer so I pushed that.

@Ben2W Ben2W force-pushed the fuse-allow-privileged branch from 84f41da to 262df01 Compare April 30, 2026 18:21
@Ben2W Ben2W changed the title [wrangler/miniflare] Add enable_containers_privileged_mode for FUSE in local dev [wrangler/miniflare] Add privileged_containers / --privileged-containers for FUSE in local dev Apr 30, 2026
@Ben2W Ben2W force-pushed the fuse-allow-privileged branch from 262df01 to 0bfc42c Compare May 1, 2026 07:29
Comment on lines +194 to +205
imageName?: string;
allowPrivileged?: boolean;
};

@Ben2W Ben2W May 1, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up: Worker_DurableObjectNamespace wasn't declaring a container field on main at all, even though miniflare was already passing container: { imageName } through to workerd at runtime. Happy to pull this out of the PR if you'd rather keep it scoped, let me know.

@Ben2W Ben2W marked this pull request as ready for review May 1, 2026 07:47
@Ben2W Ben2W requested a review from workers-devprod as a code owner May 1, 2026 07:47
@workers-devprod workers-devprod requested review from a team and petebacondarwin and removed request for a team May 1, 2026 07:47
@workers-devprod

workers-devprod commented May 1, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

@petebacondarwin petebacondarwin added package:miniflare Relating to Miniflare product:containers Relating to Cloudflare Containers: https://developers.cloudflare.com/containers/ blocked Blocked on other work labels May 1, 2026

@petebacondarwin petebacondarwin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but marking as draft and blocked until cloudflare/workerd#6596 has landed and we have bumped the workerd version in workers-sdk.

@@ -771,6 +774,14 @@ function normalizeAndValidateDev(
"boolean"
);

validateOptionalProperty(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just realised that the way that normalizeAndValidateDev() is structured, all these calls to validateOptionaProperty() are pointless, since we have already forced the values not to be undefined in the assignment from rawDev above. So these could just have been validateRequiredProperty().

Anyway, nothing to do with this PR.

@@ -724,6 +726,7 @@ function normalizeAndValidateDev(
: local_protocol,
host,
enable_containers = enableContainersArg ?? true,
privileged_containers = privilegedContainersArg ?? false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I just realised that this means that the Wrangler config value will override any command line arg, right?

Is that what you want? Classically the ordering tends to be command line arg > env var > config value > default.

I appreciate that this is how many of the other properties are being computed too. Although notably port is validated "correctly". (It's default is set later

port:
input.dev?.server?.port ??
config.dev.port ??
(await getLocalPort(initialIpListenCheck)),
)

@petebacondarwin petebacondarwin marked this pull request as draft May 1, 2026 08:36
@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk May 1, 2026

@workers-devprod workers-devprod left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codeowners reviews satisfied

@github-project-automation github-project-automation Bot moved this from Untriaged to Approved in workers-sdk May 1, 2026
…ers for FUSE in local dev

Containers using FUSE work in production but break in `wrangler dev`
because workerd doesn't grant CAP_SYS_ADMIN, mount /dev/fuse, or relax
AppArmor on the container it creates.

Adds an opt-in dev config flag — `dev.privileged_containers` in
wrangler.json, `--privileged-containers` on the CLI — that, when set,
has miniflare ask workerd to launch local containers with those three
permissions. Off by default; only takes effect when the user explicitly
opts in.

Pairs with the workerd change in
cloudflare/workerd#6596, which adds the
matching `allowPrivileged` field to ContainerOptions and gates the
FUSE injection on it.
Ben2W added a commit to Ben2W/workerd that referenced this pull request Jun 8, 2026
…tainers

Containers using FUSE work in production but break in `wrangler dev`
because workerd doesn't grant CAP_SYS_ADMIN, mount /dev/fuse, or relax
AppArmor on the container it creates. Without those three knobs the
mount("fuse", ...) syscall inside the container fails.

Add a new boolean to ContainerOptions in workerd.capnp, off by default:

  struct ContainerOptions {
    imageName       @0 :Text;
    allowPrivileged @1 :Bool = false;  # NEW
  }

When set on a DurableObjectNamespace's container config, ContainerClient
adds the minimum HostConfig fields needed for FUSE to the
POST /containers/create payload:

  - CapAdd: ["SYS_ADMIN"]                 (for the mount() syscall)
  - Devices: [/dev/fuse]                  (so the kernel has something to open)
  - SecurityOpt: ["apparmor:unconfined"]  (bypasses the default docker
                                           apparmor profile's mount block)

No Privileged=true. Mirrors what Cloudflare's production container
runtime effectively provides for FUSE workers, without granting all
Linux capabilities or full /dev passthrough.

server.c++ reads the flag from the per-DO ContainerOptions config and
threads it through to the ContainerClient constructor. Off by default
means existing configs are unaffected. Only the local Docker code path
is touched; production is unaffected.

Also adds missing $Json.name annotations to docker_api::DeviceMapping
fields in docker-api.capnp so they serialize as PathOnHost /
PathInContainer / CgroupPermissions to match the Docker API. This is
load-bearing for the Devices bind above — without it, the entry
serializes as camelCase and Docker silently ignores it. Before this PR
no call site populated Devices, so the missing annotations were latent.

Follows the existing pattern of createSidecarContainer() which already
sets CapAdd=[NET_ADMIN] for its own need at container-client.c++:1958.

The matching workers-sdk PR
(cloudflare/workers-sdk#13748) adds
`dev.enable_containers_privileged_mode` and
`--enable-containers-privileged-mode` so wrangler / vite-plugin users
can opt in.

End-to-end reproduction (libc mount("fuse", ...) syscall succeeding
inside the container with the flag set, and failing without it) at
https://github.com/Ben2W/workerd-fuse-local-repro

Signed-off-by: Ben Werner <bewerner23@gmail.com>
Ben2W added a commit to Ben2W/workerd that referenced this pull request Jun 8, 2026
…tainers

Containers using FUSE work in production but break in `wrangler dev`
because workerd doesn't grant CAP_SYS_ADMIN, mount /dev/fuse, or relax
AppArmor on the container it creates. Without those three knobs the
mount("fuse", ...) syscall inside the container fails.

Add a new boolean to ContainerOptions in workerd.capnp, off by default:

  struct ContainerOptions {
    imageName       @0 :Text;
    allowPrivileged @1 :Bool = false;  # NEW
  }

When set on a DurableObjectNamespace's container config, ContainerClient
adds the minimum HostConfig fields needed for FUSE to the
POST /containers/create payload:

  - CapAdd: ["SYS_ADMIN"]                 (for the mount() syscall)
  - Devices: [/dev/fuse]                  (so the kernel has something to open)
  - SecurityOpt: ["apparmor:unconfined"]  (bypasses the default docker
                                           apparmor profile's mount block)

No Privileged=true. Mirrors what Cloudflare's production container
runtime effectively provides for FUSE workers, without granting all
Linux capabilities or full /dev passthrough.

server.c++ reads the flag from the per-DO ContainerOptions config and
threads it through to the ContainerClient constructor. Off by default
means existing configs are unaffected. Only the local Docker code path
is touched; production is unaffected.

Also adds missing $Json.name annotations to docker_api::DeviceMapping
fields in docker-api.capnp so they serialize as PathOnHost /
PathInContainer / CgroupPermissions to match the Docker API. This is
load-bearing for the Devices bind above — without it, the entry
serializes as camelCase and Docker silently ignores it. Before this PR
no call site populated Devices, so the missing annotations were latent.

Follows the existing pattern of createSidecarContainer() which already
sets CapAdd=[NET_ADMIN] for its own need at container-client.c++:1958.

The matching workers-sdk PR
(cloudflare/workers-sdk#13748) adds
`dev.enable_containers_privileged_mode` and
`--enable-containers-privileged-mode` so wrangler / vite-plugin users
can opt in.

End-to-end reproduction (libc mount("fuse", ...) syscall succeeding
inside the container with the flag set, and failing without it) at
https://github.com/Ben2W/workerd-fuse-local-repro

Signed-off-by: Ben Werner <bewerner23@gmail.com>
@Ben2W Ben2W force-pushed the fuse-allow-privileged branch from 0bfc42c to 37d5ae5 Compare June 8, 2026 23:31
Ben2W added a commit to Ben2W/cloudflare-docs that referenced this pull request Jun 9, 2026
Adds a callout to the R2 FUSE example, a new troubleshooting section
in the Containers local-dev page, and a changelog entry, all explaining
that FUSE in `wrangler dev` requires the new opt-in flag
(`dev.privileged_containers` / `--privileged-containers`).

Pairs with cloudflare/workerd#6596 and cloudflare/workers-sdk#13748.
Resolves cloudflare/workerd#5609.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked Blocked on other work package:miniflare Relating to Miniflare product:containers Relating to Cloudflare Containers: https://developers.cloudflare.com/containers/

Projects

Status: Approved

Development

Successfully merging this pull request may close these issues.

🐛 Bug Report — Runtime APIs: No privileged mode option for Containers local development

4 participants