Skip to content

Commit 38469d2

Browse files
committed
Enable FUSE in local-dev Docker containers
In ContainerClient::createContainer(), set Privileged=true, CapAdd=[SYS_ADMIN], Devices=[/dev/fuse], and SecurityOpt=[apparmor:unconfined] on the user's app container. Mirrors what Cloudflare's production container runtime effectively provides, so Workers that use FUSE (via e.g. the Containers FUSE binding) work in local dev the way they work in production. Also adds missing $Json.name annotations to docker_api::DeviceMapping fields so they serialize as PathOnHost / PathInContainer / CgroupPermissions to match the Docker API. Before this change no call site populated Devices, so the missing annotations were latent. Follows the same pattern as createSidecarContainer(), which already sets CapAdd=[NET_ADMIN] for its own need at container-client.c++:1958-1960. End-to-end reproduction (FUSE mount syscall actually succeeding inside the container) in https://github.com/Ben2W/workerd-fuse-local-repro Signed-off-by: Ben Werner <bewerner23@gmail.com>
1 parent 22c1ca0 commit 38469d2

2 files changed

Lines changed: 32 additions & 3 deletions

File tree

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,35 @@ 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 by running the user's container privileged with /dev/fuse
1711+
// exposed. This only affects local development via workerd's Docker
1712+
// client; production is unaffected.
1713+
hostConfig.setPrivileged(true);
1714+
{
1715+
auto capAdd = hostConfig.initCapAdd(1);
1716+
capAdd.set(0, "SYS_ADMIN");
1717+
}
1718+
{
1719+
auto devices = hostConfig.initDevices(1);
1720+
auto fuseDev = devices[0];
1721+
fuseDev.setPathOnHost("/dev/fuse");
1722+
fuseDev.setPathInContainer("/dev/fuse");
1723+
fuseDev.setCgroupPermissions("rwm");
1724+
}
1725+
{
1726+
auto securityOpt = hostConfig.initSecurityOpt(1);
1727+
securityOpt.set(0, "apparmor:unconfined");
1728+
}
1729+
17011730
auto response = co_await dockerApiRequest(network, kj::str(dockerPath), kj::HttpMethod::POST,
17021731
kj::str("/containers/create?name=", containerName), codec.encode(jsonRoot));
17031732

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)