Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AI_AGENT_DISCLOSURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
> *"This contribution was prepared by an AI agent acting on a human's behalf.
> The human submitter may not have independently reviewed or tested the change."*
2026-09-05
17 changes: 13 additions & 4 deletions cmd/compose/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backend
flags.StringArrayVar(&up.attach, "attach", []string{}, "Restrict attaching to the specified services. Incompatible with --attach-dependencies.")
flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Do not attach (stream logs) to the specified services")
flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Automatically attach to log output of dependent services")
flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode, unless combined with --attach or --attach-dependencies to also stream logs while waiting.")
flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration in seconds to wait for the project to be running|healthy")
flags.BoolVarP(&up.watch, "watch", "w", false, "Watch source code and rebuild/refresh containers when files are updated.")
flags.BoolVar(&up.navigationMenu, "menu", false, "Enable interactive shortcuts when running attached. Incompatible with --detach. Can also be enable/disable by setting COMPOSE_MENU environment var.")
Expand All @@ -197,10 +197,19 @@ func validateFlags(up *upOptions, create *createOptions) error {
return fmt.Errorf("--abort-on-container-failure cannot be combined with --abort-on-container-exit")
}
if up.wait {
if up.attachDependencies || up.cascadeStop || len(up.attach) > 0 {
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
if up.cascadeStop || up.cascadeFail {
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit or --abort-on-container-failure")
}
if up.watch {
return fmt.Errorf("--wait cannot be combined with --watch")
}
if !up.attachDependencies && len(up.attach) == 0 {
// Nothing was asked to stream logs, so --wait keeps its historical
// silent, detached behavior. Passing --attach or
// --attach-dependencies alongside --wait streams logs while
// waiting, then detaches once the wait condition is met.
up.Detach = true
}
up.Detach = true
}
if create.Build && create.noBuild {
return fmt.Errorf("--build and --no-build are incompatible")
Expand Down
39 changes: 39 additions & 0 deletions cmd/compose/up_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,42 @@ services:
assert.Assert(t, strings.Contains(output, "LXKNS_PORT"), output)
assert.Assert(t, !strings.Contains(fmt.Sprint(err), "invalid ip address"), fmt.Sprint(err))
}

// https://github.com/docker/compose/issues/9122
// --wait must be combinable with --attach/--attach-dependencies, to stream
// logs while waiting instead of only polling silently.
func TestValidateFlags_WaitWithAttach(t *testing.T) {
up := &upOptions{wait: true, attach: []string{"web"}}
err := validateFlags(up, &createOptions{})
assert.NilError(t, err)
assert.Assert(t, !up.Detach, "--wait --attach must not force detached mode")

up = &upOptions{wait: true, attachDependencies: true}
err = validateFlags(up, &createOptions{})
assert.NilError(t, err)
assert.Assert(t, !up.Detach, "--wait --attach-dependencies must not force detached mode")
}

func TestValidateFlags_WaitAloneStaysDetached(t *testing.T) {
up := &upOptions{wait: true}
err := validateFlags(up, &createOptions{})
assert.NilError(t, err)
assert.Assert(t, up.Detach, "--wait alone must keep its historical detached behavior")
}

func TestValidateFlags_WaitIncompatibleFlags(t *testing.T) {
tests := []struct {
name string
up *upOptions
}{
{"abort-on-container-exit", &upOptions{wait: true, cascadeStop: true}},
{"abort-on-container-failure", &upOptions{wait: true, cascadeFail: true}},
{"watch", &upOptions{wait: true, watch: true}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateFlags(tc.up, &createOptions{})
assert.ErrorContains(t, err, "--wait cannot be combined with")
})
}
}
2 changes: 1 addition & 1 deletion docs/reference/compose_up.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ hook runs again. Running `compose down` also removes them.
| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. |
| `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running |
| `--timestamps` | `bool` | | Show timestamps |
| `--wait` | `bool` | | Wait for services to be running\|healthy. Implies detached mode. |
| `--wait` | `bool` | | Wait for services to be running\|healthy. Implies detached mode, unless combined with --attach or --attach-dependencies to also stream logs while waiting. |
| `--wait-timeout` | `int` | `0` | Maximum duration in seconds to wait for the project to be running\|healthy |
| `-w`, `--watch` | `bool` | | Watch source code and rebuild/refresh containers when files are updated. |
| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts and run non-interactively |
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/docker_compose_up.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ options:
- option: wait
value_type: bool
default_value: "false"
description: Wait for services to be running|healthy. Implies detached mode.
description: |
Wait for services to be running|healthy. Implies detached mode, unless combined with --attach or --attach-dependencies to also stream logs while waiting.
deprecated: false
hidden: false
experimental: false
Expand Down
8 changes: 8 additions & 0 deletions pkg/compose/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,14 @@ func (s *composeService) runInteractiveUp(ctx context.Context, project *types.Pr
return err
}

if options.Start.Wait && !u.isTerminated.Load() {
// --wait attached long enough to stream logs while services became
// running|healthy; detach now instead of following logs forever like
// a plain foreground `up`, exactly as if the user had pressed the
// detach shortcut. Containers are left running.
cancel()
}

_ = u.eg.Wait()
err = errors.Join(u.errs...)
if u.exitCode != 0 {
Expand Down
26 changes: 26 additions & 0 deletions pkg/e2e/compose_up_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,32 @@ func TestUpWaitTimeout(t *testing.T) {
ServiceState("app", "running"))
}

// https://github.com/docker/compose/issues/9122
// --wait combined with --attach must stream the waited-for service's logs
// while polling for health, then detach (leaving it running) once the wait
// condition is met, instead of following logs forever like a plain
// foreground `up`.
func TestWaitStreamsLogsThenDetaches(t *testing.T) {
NewScenario(t, "up --wait --attach must stream logs while waiting, then detach with the service left running").
Step("up --wait --attach prints the service's early log line and returns once healthy",
ComposeCmd("up", "--wait", "--attach", "test").Within(30*time.Second),
StdoutContains("hello-while-waiting"),
ServiceState("test", "running"),
ServiceHealthy("test"))
}

// https://github.com/docker/compose/issues/9122
// Without --attach or --attach-dependencies, --wait must keep its historical
// silent, detached behavior.
func TestWaitAloneStaysSilent(t *testing.T) {
NewScenario(t, "up --wait alone must not stream logs, preserving its historical silent behavior").
Step("up --wait completes without echoing the service's logs",
ComposeCmd("up", "--wait", "test").Within(30*time.Second),
OutputNotContains("hello-while-waiting"),
ServiceState("test", "running"),
ServiceHealthy("test"))
}

func TestUpExitCodeFrom(t *testing.T) {
NewScenario(t, "up --exit-code-from must return the selected service's exit code").
Step("up returns the failing service's code once it exits",
Expand Down
10 changes: 10 additions & 0 deletions pkg/e2e/testdata/TestWaitAloneStaysSilent/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
services:
test:
image: alpine
init: true
command: sh -c "echo hello-while-waiting; sleep 3; touch /tmp/ready; sleep infinity"
healthcheck:
test: ["CMD", "test", "-f", "/tmp/ready"]
interval: 30s
start_period: 10s
start_interval: 1s
10 changes: 10 additions & 0 deletions pkg/e2e/testdata/TestWaitStreamsLogsThenDetaches/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
services:
test:
image: alpine
init: true
command: sh -c "echo hello-while-waiting; sleep 3; touch /tmp/ready; sleep infinity"
healthcheck:
test: ["CMD", "test", "-f", "/tmp/ready"]
interval: 30s
start_period: 10s
start_interval: 1s