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:
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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user