Skip to content

Commit b7e40b2

Browse files
committed
Enable FUSE in local-dev Docker containers
In ContainerClient::createContainer(), grant the minimum capabilities needed so a FUSE mount inside the user's container succeeds: - 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. Only affects workerd's local Docker code path; 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. End-to-end reproduction (libc mount("fuse", ...) syscall succeeding inside the container, with a wire-level log confirming PathOnHost etc. reach Docker correctly) at https://github.com/Ben2W/workerd-fuse-local-repro Signed-off-by: Ben Werner <bewerner23@gmail.com>
1 parent 22c1ca0 commit b7e40b2

2 files changed

Lines changed: 33 additions & 3 deletions

File tree

src/workerd/server/container-client.c++

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,36 @@ kj::Promise<void> ContainerClient::createContainer(kj::StringPtr effectiveImage,
16981698
}
16991699
}
17001700

1701+
// Local-dev FUSE parity with production.
1702+
//
1703+
// Cloudflare's production container runtime exposes /dev/fuse to workers
1704+
// and grants the capabilities required to mount it. Locally we talk to a
1705+
// stock Docker daemon, which by default does not bind /dev/fuse into the
1706+
// container and does not grant CAP_SYS_ADMIN, so `open("/dev/fuse")` and
1707+
// the subsequent `mount("fuse", ...)` syscall fail with ENOENT / EPERM.
1708+
// Workers that use FUSE (e.g. the containers FUSE binding) therefore
1709+
// work in prod but break in local dev. Mirror what prod effectively
1710+
// provides with the narrowest capabilities needed: CAP_SYS_ADMIN so the
1711+
// mount() syscall can run, a /dev/fuse device binding so the kernel has
1712+
// something to open, and apparmor unconfined so the default Docker
1713+
// apparmor profile doesn't block the mount. This only affects local
1714+
// development via workerd's Docker client; production is unaffected.
1715+
{
1716+
auto capAdd = hostConfig.initCapAdd(1);
1717+
capAdd.set(0, "SYS_ADMIN");
1718+
}
1719+
{
1720+
auto devices = hostConfig.initDevices(1);
1721+
auto fuseDev = devices[0];
1722+
fuseDev.setPathOnHost("/dev/fuse");
1723+
fuseDev.setPathInContainer("/dev/fuse");
1724+
fuseDev.setCgroupPermissions("rwm");
1725+
}
1726+
{
1727+
auto securityOpt = hostConfig.initSecurityOpt(1);
1728+
securityOpt.set(0, "apparmor:unconfined");
1729+
}
1730+
17011731
auto response = co_await dockerApiRequest(network, kj::str(dockerPath), kj::HttpMethod::POST,
17021732
kj::str("/containers/create?name=", containerName), codec.encode(jsonRoot));
17031733

src/workerd/server/docker-api.capnp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ struct Docker {
3232
}
3333

3434
struct DeviceMapping {
35-
pathOnHost @0 :Text;
36-
pathInContainer @1 :Text;
37-
cgroupPermissions @2 :Text;
35+
pathOnHost @0 :Text $Json.name("PathOnHost");
36+
pathInContainer @1 :Text $Json.name("PathInContainer");
37+
cgroupPermissions @2 :Text $Json.name("CgroupPermissions");
3838
}
3939

4040
struct DeviceRequest {

0 commit comments

Comments
 (0)