From 33f4b0bd0c8fb1d7ca376ef58fadf535e345be09 Mon Sep 17 00:00:00 2001 From: Toru Nayuki Date: Mon, 15 Jun 2026 16:04:03 +0900 Subject: [PATCH] dev_container: Support the classic Docker builder via a setting (#59288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dev container image builds default to BuildKit (`docker buildx`). Docker-compatible engines that lack an integrated BuildKit — notably [Apple Container](https://github.com/apple/container) accessed through a Docker-API bridge — cannot resolve a locally-built image in a `FROM`, which breaks the dev container build: Zed builds the features image, then a second `FROM ` build (the UID remap), and BuildKit's docker-container/remote drivers can't see the daemon's local image, failing with `pull access denied`. The daemon's *classic* builder resolves locally-built images, and Zed already generates a BuildKit-free Dockerfile for the non-BuildKit path (multi-stage `FROM` + `COPY --from`, no `RUN --mount`). This wires that path to a setting and makes the remaining build invocations actually use the classic builder. ## Changes - Add a `dev_container_use_buildkit` setting (tri-state): - unset → auto-detect via the `docker buildx` plugin (current behavior) - `false` → force the classic builder - `true` → force BuildKit - When the classic builder is selected, pass `DOCKER_BUILDKIT=0` (and `COMPOSE_DOCKER_CLI_BUILD=0` for compose) to the feature-content build, the compose build, and the UID-remap build, so multi-stage `FROM` of a locally-built image resolves through the daemon's classic builder. - Document the setting in `docs/src/dev-containers.md`. With this setting plus a Docker-API bridge, a real Rails dev container (compose: app + mysql + valkey, with features) builds and starts on Apple Container. ## How to Review - `settings_content.rs` / `dev_container/src/lib.rs` — the new setting and its plumbing into `DevContainerContext`. - `docker.rs` — `Docker::new` honors the setting (`supports_compose_buildkit`), `docker_compose_build` selects the classic builder; unit test `use_buildkit_setting_overrides_buildx_detection`. - `devcontainer_manifest.rs` — `DOCKER_BUILDKIT=0` for the feature-content and UID-remap builds. ## Self-Review Checklist - [x] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [ ] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Added a `dev_container_use_buildkit` setting to build dev containers with the classic Docker builder for engines without an integrated BuildKit (e.g. Apple Container) --- .../src/devcontainer_manifest.rs | 23 +++++++-- crates/dev_container/src/docker.rs | 49 +++++++++++++++++-- crates/dev_container/src/lib.rs | 8 ++- .../settings_content/src/settings_content.rs | 10 ++++ docs/src/dev-containers.md | 2 + 5 files changed, 82 insertions(+), 10 deletions(-) diff --git a/crates/dev_container/src/devcontainer_manifest.rs b/crates/dev_container/src/devcontainer_manifest.rs index afe02b4950..fb2e4f6cd6 100644 --- a/crates/dev_container/src/devcontainer_manifest.rs +++ b/crates/dev_container/src/devcontainer_manifest.rs @@ -1541,6 +1541,14 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true let updated_image_tag = features_build_info.image_tag.clone(); let mut command = Command::new(self.docker_client.docker_cli()); + // Without a usable BuildKit, force the classic builder: the build's + // `FROM $BASE_IMAGE` references the locally-built features image, which + // only resolves from the daemon's image store under the classic builder. + if !self.docker_client.supports_compose_buildkit() + && self.docker_client.docker_cli() != "podman" + { + command.env("DOCKER_BUILDKIT", "0"); + } command.args(["build"]); command.args(["-f", &dockerfile_path.display().to_string()]); command.args(["-t", &updated_image_tag]); @@ -1644,6 +1652,12 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${PATH:-\3}/g' /etc/profile || true })?; let mut command = Command::new(self.docker_client.docker_cli()); + // This path runs only when BuildKit is unavailable, so force the classic + // builder: the feature content image is consumed by a later multi-stage + // `FROM`, which requires it to live in the daemon's image store. + if self.docker_client.docker_cli() != "podman" { + command.env("DOCKER_BUILDKIT", "0"); + } command.args([ "build", "-t", @@ -2375,9 +2389,9 @@ pub(crate) async fn read_devcontainer_configuration( environment: HashMap, ) -> Result { let docker = if context.use_podman { - Docker::new("podman").await + Docker::new("podman", context.use_buildkit).await } else { - Docker::new("docker").await + Docker::new("docker", context.use_buildkit).await }; let mut dev_container = DevContainerManifest::new( context, @@ -2399,9 +2413,9 @@ pub(crate) async fn spawn_dev_container( local_project_path: &Path, ) -> Result { let docker = if context.use_podman { - Docker::new("podman").await + Docker::new("podman", context.use_buildkit).await } else { - Docker::new("docker").await + Docker::new("docker", context.use_buildkit).await }; let mut devcontainer_manifest = DevContainerManifest::new( context, @@ -3062,6 +3076,7 @@ mod test { let context = DevContainerContext { project_directory: SanitizedPath::cast_arc(project_path), use_podman: false, + use_buildkit: None, fs: fs.clone(), http_client: http_client.clone(), environment: project_environment.downgrade(), diff --git a/crates/dev_container/src/docker.rs b/crates/dev_container/src/docker.rs index 6a583681ac..6e3e82dc76 100644 --- a/crates/dev_container/src/docker.rs +++ b/crates/dev_container/src/docker.rs @@ -192,9 +192,18 @@ impl DockerInspect { } impl Docker { - pub(crate) async fn new(docker_cli: &str) -> Self { + pub(crate) async fn new(docker_cli: &str, use_buildkit: Option) -> Self { let has_buildx = if docker_cli == "podman" { false + } else if let Some(use_buildkit) = use_buildkit { + // Honor the explicit `dev_container_use_buildkit` setting. Setting it + // to `false` forces the classic Docker builder for Docker-compatible + // engines that lack an integrated BuildKit (e.g. Apple Container via + // a Docker-API bridge), where BuildKit builds cannot resolve + // locally-built images. The classic builder builds the feature + // content as an image and references it with an ordinary + // multi-stage `FROM`. + use_buildkit } else { let output = Command::new(docker_cli) .args(["buildx", "version"]) @@ -204,7 +213,7 @@ impl Docker { }; if !has_buildx && docker_cli != "podman" { log::info!( - "docker buildx not found; dev container builds will use the scratch-image fallback" + "Using the classic Docker builder for dev container builds (BuildKit unavailable or disabled)" ); } Self { @@ -295,7 +304,15 @@ impl DockerClient for Docker { ) -> Result<(), DevContainerError> { let mut command = Command::new(&self.docker_cli); if !self.is_podman() { - command.env("DOCKER_BUILDKIT", "1"); + if self.has_buildx { + command.env("DOCKER_BUILDKIT", "1"); + } else { + // Without a usable BuildKit, build through the classic builder so + // multi-stage `FROM` of locally-built images (the feature content + // image) resolves from the daemon's image store. + command.env("DOCKER_BUILDKIT", "0"); + command.env("COMPOSE_DOCKER_CLI_BUILD", "0"); + } } command.args(&["compose", "--project-name", project_name]); for docker_compose_file in config_files { @@ -694,11 +711,33 @@ mod test { devcontainer_api::DevContainerError, devcontainer_json::MountDefinition, docker::{ - Docker, DockerComposeConfig, DockerComposeService, DockerComposeServicePort, - DockerComposeVolume, DockerInspect, DockerPs, parse_find_process_output, + Docker, DockerClient, DockerComposeConfig, DockerComposeService, + DockerComposeServicePort, DockerComposeVolume, DockerInspect, DockerPs, + parse_find_process_output, }, }; + #[test] + fn use_buildkit_setting_overrides_buildx_detection() { + // `Some(_)` short-circuits the `buildx version` probe, so these run + // without invoking docker. + let forced_off = futures::executor::block_on(Docker::new("docker", Some(false))); + assert!( + !forced_off.supports_compose_buildkit(), + "use_buildkit=false must force the classic builder" + ); + + let forced_on = futures::executor::block_on(Docker::new("docker", Some(true))); + assert!( + forced_on.supports_compose_buildkit(), + "use_buildkit=true must enable BuildKit" + ); + + // podman never supports the BuildKit/buildx path, regardless of the setting. + let podman = futures::executor::block_on(Docker::new("podman", Some(true))); + assert!(!podman.supports_compose_buildkit()); + } + #[test] fn should_parse_simple_env_var() { let config = super::DockerInspectConfig { diff --git a/crates/dev_container/src/lib.rs b/crates/dev_container/src/lib.rs index 601394c777..833caa830f 100644 --- a/crates/dev_container/src/lib.rs +++ b/crates/dev_container/src/lib.rs @@ -98,6 +98,7 @@ fn get_safe_id(input: &str) -> String { pub struct DevContainerContext { pub project_directory: Arc, pub use_podman: bool, + pub use_buildkit: Option, pub fs: Arc, pub http_client: Arc, pub environment: WeakEntity, @@ -106,13 +107,16 @@ pub struct DevContainerContext { impl DevContainerContext { pub fn from_workspace(workspace: &Workspace, cx: &App) -> Option { let project_directory = workspace.project().read(cx).active_project_directory(cx)?; - let use_podman = DevContainerSettings::get_global(cx).use_podman; + let settings = DevContainerSettings::get_global(cx); + let use_podman = settings.use_podman; + let use_buildkit = settings.use_buildkit; let http_client = cx.http_client().clone(); let fs = workspace.app_state().fs.clone(); let environment = workspace.project().read(cx).environment().downgrade(); Some(Self { project_directory, use_podman, + use_buildkit, fs, http_client, environment, @@ -134,6 +138,7 @@ impl DevContainerContext { #[derive(RegisterSetting)] struct DevContainerSettings { use_podman: bool, + use_buildkit: Option, } pub fn use_podman(cx: &App) -> bool { @@ -144,6 +149,7 @@ impl Settings for DevContainerSettings { fn from_settings(content: &settings::SettingsContent) -> Self { Self { use_podman: content.remote.use_podman.unwrap_or(false), + use_buildkit: content.remote.dev_container_use_buildkit, } } } diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index 9ac8b4b14f..0ae76ccf56 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -1135,6 +1135,16 @@ pub struct RemoteSettingsContent { pub dev_container_connections: Option>, pub read_ssh_config: Option, pub use_podman: Option, + /// Whether to build dev container images with BuildKit. + /// + /// When unset, Zed auto-detects BuildKit by probing for the `buildx` CLI + /// plugin. Set to `false` to force the classic Docker builder, which is + /// required for Docker-compatible engines that lack an integrated BuildKit + /// (e.g. Apple Container via a Docker-API bridge), where BuildKit builds + /// cannot resolve locally-built images. + /// + /// Default: null (auto-detect) + pub dev_container_use_buildkit: Option, } #[with_fallible_options] diff --git a/docs/src/dev-containers.md b/docs/src/dev-containers.md index 3b0874812b..e3765ab522 100644 --- a/docs/src/dev-containers.md +++ b/docs/src/dev-containers.md @@ -14,6 +14,8 @@ If your repository includes a `.devcontainer/devcontainer.json` file, Zed can op - Docker or Podman must be installed and available in your `PATH`. If you use `podman`, you must set the `use_podman` setting in your Zed settings.json to true. - Your project must contain a `.devcontainer/devcontainer.json` directory/file. +By default Zed builds dev container images with BuildKit when the `docker buildx` plugin is available. If your Docker-compatible engine lacks an integrated BuildKit (for example, Apple Container accessed through a Docker-API bridge), set `"dev_container_use_buildkit": false` in your settings.json to use the classic Docker builder instead. + ## Using Dev Containers in Zed ### Automatic prompt