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`.
This commit is contained in:
Yauhen 2026-05-29 07:12:28 +02:00 committed by GitHub
parent e5f5767d2c
commit d3070bbc0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 138 additions and 3 deletions

View file

@ -232,7 +232,7 @@ pub(crate) struct DevContainer {
#[serde(default, deserialize_with = "deserialize_string_or_array")]
pub(crate) docker_compose_file: Option<Vec<String>>,
pub(crate) service: Option<String>,
run_services: Option<Vec<String>>,
pub(crate) run_services: Option<Vec<String>>,
pub(crate) initialize_command: Option<LifecycleScript>,
pub(crate) on_create_command: Option<LifecycleScript>,
pub(crate) update_content_command: Option<LifecycleScript>,

View file

@ -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<PathBuf>,
_project_name: &str,
_services: Option<&Vec<String>>,
) -> Result<(), DevContainerError> {
Ok(())
}

View file

@ -291,6 +291,7 @@ impl DockerClient for Docker {
&self,
config_files: &Vec<PathBuf>,
project_name: &str,
services: Option<&Vec<String>>,
) -> 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<PathBuf>,
project_name: &str,
services: Option<&Vec<String>>,
) -> Result<(), DevContainerError>;
async fn run_docker_exec(
&self,