dev_container: Support the classic Docker builder via a setting (#59288)

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 <features-image>` 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)
This commit is contained in:
Toru Nayuki authored and GitHub committed 2026-06-15 07:04:03 +00:00
1 parent b600f44d69
commit 33f4b0bd0c
5 files changed
+82 -10

No files matched your search

@@ -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<String, String>,
) -> Result<DevContainer, DevContainerError> {
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<DevContainerUp, DevContainerError> {
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(),
+44 -5
View File
@@ -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<bool>) -> 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 {
+7 -1
View File
@@ -98,6 +98,7 @@ fn get_safe_id(input: &str) -> String {
pub struct DevContainerContext {
pub project_directory: Arc<Path>,
pub use_podman: bool,
pub use_buildkit: Option<bool>,
pub fs: Arc<dyn Fs>,
pub http_client: Arc<dyn HttpClient>,
pub environment: WeakEntity<ProjectEnvironment>,
@@ -106,13 +107,16 @@ pub struct DevContainerContext {
impl DevContainerContext {
pub fn from_workspace(workspace: &Workspace, cx: &App) -> Option<Self> {
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<bool>,
}
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,
}
}
}
@@ -1135,6 +1135,16 @@ pub struct RemoteSettingsContent {
pub dev_container_connections: Option<Vec<DevContainerConnection>>,
pub read_ssh_config: Option<bool>,
pub use_podman: Option<bool>,
/// 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<bool>,
}
#[with_fallible_options]
+2
View File
@@ -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