Skip to content

Commit 37d5ae5

Browse files
committed
[wrangler/miniflare] Add privileged_containers / --privileged-containers 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.
1 parent 204f90a commit 37d5ae5

16 files changed

Lines changed: 113 additions & 0 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"wrangler": minor
3+
"miniflare": minor
4+
---
5+
6+
Add `dev.privileged_containers` config and `--privileged-containers` CLI flag for FUSE in local dev
7+
8+
When set, miniflare launches local containers with the elevated permissions FUSE requires (`CAP_SYS_ADMIN`, `/dev/fuse`, AppArmor unconfined). Off by default.

packages/miniflare/src/plugins/do/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import type {
1717
// Options for a container attached to the DO
1818
export const DOContainerOptionsSchema = z.object({
1919
imageName: z.string(),
20+
// When true, workerd creates the container with the elevated privileges
21+
// needed for FUSE (CAP_SYS_ADMIN, /dev/fuse, AppArmor unconfined).
22+
allowPrivileged: z.boolean().optional(),
2023
});
2124
export type DOContainerOptions = z.infer<typeof DOContainerOptionsSchema>;
2225

packages/miniflare/src/runtime/config/workerd.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,14 @@ export type Worker_DurableObjectNamespace = {
195195
className?: string;
196196
preventEviction?: boolean;
197197
enableSql?: boolean;
198+
container?: Worker_ContainerOptions;
198199
} & ({ uniqueKey?: string } | { ephemeralLocal?: Void });
199200

201+
export type Worker_ContainerOptions = {
202+
imageName?: string;
203+
allowPrivileged?: boolean;
204+
};
205+
200206
export type ExternalServer = { address?: string } & (
201207
| { http: HttpOptions }
202208
| { https: ExternalServer_Https }

packages/workers-utils/src/config/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ export interface DevConfig {
254254
*/
255255
enable_containers: boolean;
256256

257+
/**
258+
* When developing, whether to launch containers with the elevated privileges
259+
* required for FUSE mounts (CAP_SYS_ADMIN, /dev/fuse, AppArmor unconfined).
260+
* Off by default — only enable when your container needs FUSE locally.
261+
*
262+
* @default false
263+
*/
264+
privileged_containers: boolean;
265+
257266
/**
258267
* Either the Docker unix socket i.e. `unix:///var/run/docker.sock` or a full configuration.
259268
* Note that windows is only supported via WSL at the moment
@@ -312,6 +321,7 @@ export const defaultWranglerConfig: Config = {
312321
host: undefined,
313322
// Note this one is also workers only
314323
enable_containers: true,
324+
privileged_containers: false,
315325
container_engine: undefined,
316326
generate_types: false,
317327
},

packages/workers-utils/src/config/validation.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export type NormalizeAndValidateConfigArgs = {
237237
upstreamProtocol?: string;
238238
script?: string;
239239
enableContainers?: boolean;
240+
privilegedContainers?: boolean;
240241
generateTypes?: boolean;
241242
};
242243

@@ -692,6 +693,7 @@ function normalizeAndValidateDev(
692693
upstreamProtocol: upstreamProtocolArg,
693694
remote: remoteArg,
694695
enableContainers: enableContainersArg,
696+
privilegedContainers: privilegedContainersArg,
695697
generateTypes: generateTypesArg,
696698
} = args;
697699
assert(
@@ -709,6 +711,10 @@ function normalizeAndValidateDev(
709711
enableContainersArg === undefined ||
710712
typeof enableContainersArg === "boolean"
711713
);
714+
assert(
715+
privilegedContainersArg === undefined ||
716+
typeof privilegedContainersArg === "boolean"
717+
);
712718
assert(
713719
generateTypesArg === undefined || typeof generateTypesArg === "boolean"
714720
);
@@ -731,6 +737,7 @@ function normalizeAndValidateDev(
731737
: local_protocol,
732738
host,
733739
enable_containers = enableContainersArg ?? true,
740+
privileged_containers = privilegedContainersArg ?? false,
734741
container_engine,
735742
generate_types = generateTypesArg ?? false,
736743
...rest
@@ -778,6 +785,14 @@ function normalizeAndValidateDev(
778785
"boolean"
779786
);
780787

788+
validateOptionalProperty(
789+
diagnostics,
790+
"dev",
791+
"privileged_containers",
792+
privileged_containers,
793+
"boolean"
794+
);
795+
781796
validateOptionalProperty(
782797
diagnostics,
783798
"dev",
@@ -803,6 +818,7 @@ function normalizeAndValidateDev(
803818
upstream_protocol,
804819
host,
805820
enable_containers,
821+
privileged_containers,
806822
container_engine,
807823
generate_types,
808824
};

packages/wrangler/src/__tests__/config-validation-pages.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ describe("validatePagesConfig()", () => {
194194
upstream_protocol: "https",
195195
host: "test-host",
196196
enable_containers: false,
197+
privileged_containers: false,
197198
container_engine: undefined,
198199
generate_types: false,
199200
},

packages/wrangler/src/__tests__/dev.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1679,6 +1679,44 @@ describe.sequential("wrangler dev", () => {
16791679
});
16801680
});
16811681

1682+
describe("privileged containers", () => {
1683+
it("should default to false", async ({ expect }) => {
1684+
writeWranglerConfig({
1685+
main: "index.js",
1686+
});
1687+
fs.writeFileSync("index.js", `export default {};`);
1688+
const config = await runWranglerUntilConfig("dev");
1689+
expect(config.dev.privilegedContainers).toBe(false);
1690+
});
1691+
1692+
it("should be true when `dev.privileged_containers` is set in the Wrangler config", async ({
1693+
expect,
1694+
}) => {
1695+
writeWranglerConfig({
1696+
main: "index.js",
1697+
dev: {
1698+
privileged_containers: true,
1699+
},
1700+
});
1701+
fs.writeFileSync("index.js", `export default {};`);
1702+
const config = await runWranglerUntilConfig("dev");
1703+
expect(config.dev.privilegedContainers).toBe(true);
1704+
});
1705+
1706+
it("should be true when `--privileged-containers` is passed on the CLI", async ({
1707+
expect,
1708+
}) => {
1709+
writeWranglerConfig({
1710+
main: "index.js",
1711+
});
1712+
fs.writeFileSync("index.js", `export default {};`);
1713+
const config = await runWranglerUntilConfig(
1714+
"dev --privileged-containers"
1715+
);
1716+
expect(config.dev.privilegedContainers).toBe(true);
1717+
});
1718+
});
1719+
16821720
describe("durable_objects", () => {
16831721
it("should warn if there are remote Durable Objects, or missing migrations for local Durable Objects", async ({
16841722
expect,

packages/wrangler/src/api/dev.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export async function unstable_dev(
164164
d1Databases,
165165
disableDevRegistry,
166166
testScheduled: testScheduled ?? false,
167+
privilegedContainers: false,
167168
enablePagesAssetsServiceBinding,
168169
forceLocal,
169170
liveReload,

packages/wrangler/src/api/integrations/platform/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ async function getMiniflareOptionsFromConfig(args: {
296296
),
297297
containerBuildId: undefined,
298298
enableContainers: config.dev.enable_containers,
299+
privilegedContainers:
300+
config.dev.privileged_containers,
299301
},
300302
remoteProxyConnectionString
301303
);
@@ -467,6 +469,8 @@ export function unstable_getMiniflareWorkerOptions(
467469
containerDOClassNames,
468470
containerBuildId: options?.containerBuildId,
469471
enableContainers,
472+
privilegedContainers:
473+
config.dev.privileged_containers,
470474
},
471475
options?.remoteProxyConnectionString
472476
);

packages/wrangler/src/api/startDevWorker/ConfigController.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ async function resolveDevConfig(
166166
inferOriginFromRoutes: input.dev?.inferOriginFromRoutes ?? true,
167167
enableContainers:
168168
input.dev?.enableContainers ?? config.dev.enable_containers,
169+
privilegedContainers:
170+
input.dev?.privilegedContainers ??
171+
config.dev.privileged_containers,
169172
dockerPath: input.dev?.dockerPath ?? getDockerPath(),
170173
containerEngine: useContainers
171174
? (input.dev?.containerEngine ??

0 commit comments

Comments
 (0)