From d3070bbc0d3e48bb27595f08b9051c80e6e63d15 Mon Sep 17 00:00:00 2001 From: Yauhen Date: Fri, 29 May 2026 07:12:28 +0200 Subject: [PATCH] dev_container: Respect runServices for Docker Compose (#56293) This PR fixes Docker Compose dev containers starting every service in the compose project, even when `devcontainer.json` specifies `runServices`. Previously, Zed deserialized `runServices` but did not use it when invoking Docker Compose. The startup command was: ```sh docker compose ... up -d ``` With no service operands, Compose starts every enabled service in the project. This means unrelated services are started even when the devcontainer config asks to run only the primary service and its dependencies. The fix propagates `runServices` into the Docker Compose build/start path so Zed invokes Compose with the requested services: ```sh docker compose ... up -d devcontainer ``` Compose will still start services required by `depends_on`, but unrelated services are left untouched. **Reproduction** `.devcontainer/devcontainer.json`: ```json { "name": "Run Services", "dockerComposeFile": "../compose.yml", "service": "devcontainer", "runServices": ["devcontainer"], "workspaceFolder": "/workspace" } ``` `compose.yml`: ```yaml services: devcontainer: image: ubuntu:24.04 command: sleep infinity volumes: - .:/workspace depends_on: - database database: image: postgres:16-alpine environment: POSTGRES_PASSWORD: postgres unrelated: image: nginx:alpine ``` **Expected**: Zed starts `devcontainer` and `database`. **Before this fix**: Zed also starts `unrelated`. **After this fix**: `unrelated` remains stopped. Closes: https://github.com/zed-industries/zed/issues/57279 Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] 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: - Fixed Docker Compose dev containers starting services not listed in `runServices`. --- crates/dev_container/src/devcontainer_json.rs | 2 +- .../src/devcontainer_manifest.rs | 134 +++++++++++++++++- crates/dev_container/src/docker.rs | 5 + 3 files changed, 138 insertions(+), 3 deletions(-) diff --git a/crates/dev_container/src/devcontainer_json.rs b/crates/dev_container/src/devcontainer_json.rs index 6033ca29f0..c8573b6b9c 100644 --- a/crates/dev_container/src/devcontainer_json.rs +++ b/crates/dev_container/src/devcontainer_json.rs @@ -232,7 +232,7 @@ pub(crate) struct DevContainer { #[serde(default, deserialize_with = "deserialize_string_or_array")] pub(crate) docker_compose_file: Option>, pub(crate) service: Option, - run_services: Option>, + pub(crate) run_services: Option>, pub(crate) initialize_command: Option, pub(crate) on_create_command: Option, pub(crate) update_content_command: Option, diff --git a/crates/dev_container/src/devcontainer_manifest.rs b/crates/dev_container/src/devcontainer_manifest.rs index cc8fbedb14..0c38538657 100644 --- a/crates/dev_container/src/devcontainer_manifest.rs +++ b/crates/dev_container/src/devcontainer_manifest.rs @@ -1058,7 +1058,11 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true let project_name = self.project_name().await?; self.docker_client - .docker_compose_build(&docker_compose_resources.files, &project_name) + .docker_compose_build( + &docker_compose_resources.files, + &project_name, + dev_container.run_services.as_ref(), + ) .await?; ( self.docker_client @@ -1151,7 +1155,11 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true let project_name = self.project_name().await?; self.docker_client - .docker_compose_build(&docker_compose_resources.files, &project_name) + .docker_compose_build( + &docker_compose_resources.files, + &project_name, + dev_container.run_services.as_ref(), + ) .await?; ( @@ -1785,6 +1793,9 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${PATH:-\3}/g' /etc/profile || true command.args(&["-f", &docker_compose_file.display().to_string()]); } command.args(&["up", "-d"]); + if let Some(run_services) = self.dev_container().run_services.as_ref() { + command.args(run_services); + } let output = self .command_runner @@ -4783,6 +4794,111 @@ ENV DOCKER_BUILDKIT=1 ); } + #[gpui::test] + async fn test_spawns_only_requested_compose_services(cx: &mut TestAppContext) { + cx.executor().allow_parking(); + env_logger::try_init().ok(); + let given_devcontainer_contents = r#" + { + "name": "Devcontainer and PostgreSQL", + "dockerComposeFile": "docker-compose.yml", + "service": "devcontainer", + "runServices": ["devcontainer", "db"], + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "updateRemoteUserUID": false + } + "#; + let (test_dependencies, mut devcontainer_manifest) = + init_default_devcontainer_manifest(cx, given_devcontainer_contents) + .await + .unwrap(); + + test_dependencies + .fs + .atomic_write( + PathBuf::from(TEST_PROJECT_PATH).join(".devcontainer/docker-compose.yml"), + r#" +version: '3.8' + +x-base: &base + build: + context: . + dockerfile: Dockerfile + env_file: + - .env + +volumes: + postgres-data: + +services: + app: + <<: *base + ports: + - "3000:3000" + + devcontainer: + <<: *base + ports: + - "3000:3000" + volumes: + - ../..:/workspaces:cached + + db: + image: postgres:14.1 + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + env_file: + - .env + "# + .trim() + .to_string(), + ) + .await + .unwrap(); + + test_dependencies + .fs + .atomic_write( + PathBuf::from(TEST_PROJECT_PATH).join(".devcontainer/Dockerfile"), + r#" +FROM mcr.microsoft.com/devcontainers/rust:2-1-bookworm + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -y install clang lld \ +&& apt-get autoremove -y && apt-get clean -y + "# + .trim() + .to_string(), + ) + .await + .unwrap(); + + devcontainer_manifest.parse_nonremote_vars().unwrap(); + let _devcontainer_up = devcontainer_manifest.build_and_run().await.unwrap(); + + let docker_commands = test_dependencies + .command_runner + .commands_by_program("docker"); + let compose_up = docker_commands + .iter() + .find(|c| { + c.args.first().map(String::as_str) == Some("compose") + && c.args.iter().any(|a| a == "up") + }) + .expect("docker compose up command recorded"); + assert!( + compose_up.args.ends_with(&[ + "up".to_string(), + "-d".to_string(), + "devcontainer".to_string(), + "db".to_string(), + ]), + "compose up should target only the requested service, got: {:?}", + compose_up.args + ); + } + #[cfg(not(target_os = "windows"))] #[gpui::test] async fn test_spawns_devcontainer_with_docker_compose_and_podman(cx: &mut TestAppContext) { @@ -6067,6 +6183,19 @@ FROM docker.io/hexpm/elixir:1.21-erlang-28.4.1-debian-trixie-20260316-slim AS de return Ok(Some(DockerComposeConfig { name: None, services: HashMap::from([ + ( + "devcontainer".to_string(), + DockerComposeService { + image: Some("test_image:latest".to_string()), + volumes: vec![MountDefinition { + source: Some("../..".to_string()), + target: "/workspaces".to_string(), + mount_type: Some("bind".to_string()), + }], + command: vec!["sleep".to_string(), "infinity".to_string()], + ..Default::default() + }, + ), ( "app".to_string(), DockerComposeService { @@ -6193,6 +6322,7 @@ FROM docker.io/hexpm/elixir:1.21-erlang-28.4.1-debian-trixie-20260316-slim AS de &self, _config_files: &Vec, _project_name: &str, + _services: Option<&Vec>, ) -> Result<(), DevContainerError> { Ok(()) } diff --git a/crates/dev_container/src/docker.rs b/crates/dev_container/src/docker.rs index cccbb4373e..6a583681ac 100644 --- a/crates/dev_container/src/docker.rs +++ b/crates/dev_container/src/docker.rs @@ -291,6 +291,7 @@ impl DockerClient for Docker { &self, config_files: &Vec, project_name: &str, + services: Option<&Vec>, ) -> Result<(), DevContainerError> { let mut command = Command::new(&self.docker_cli); if !self.is_podman() { @@ -301,6 +302,9 @@ impl DockerClient for Docker { command.args(&["-f", &docker_compose_file.display().to_string()]); } command.arg("build"); + if let Some(services) = services { + command.args(services); + } let output = command.output().await.map_err(|e| { log::error!("Error running docker compose up: {e}"); @@ -457,6 +461,7 @@ pub(crate) trait DockerClient { &self, config_files: &Vec, project_name: &str, + services: Option<&Vec>, ) -> Result<(), DevContainerError>; async fn run_docker_exec( &self,