#14204·compose

`post_start` / `pre_stop` hook stdout/stderr is discarded in `up -d` and `up --wait` — only attached mode shows it, and nothing persists it

Author: maks-oleksyukCreated Sep 9, 2026Updated Sep 12, 2026
Labelskind/bug

Description

Service lifecycle hooks (post_start, pre_stop) run their command, but their stdout/stderr is only forwarded to the terminal when docker compose up runs attached (foreground). In every non-interactive form — up -d, up --wait — the hook output is dropped:

  • success: completely silent, no indication the hook ran.
  • failure: a single summary line on stderr (<service> hook exited with status <N>) plus a non-zero exit code, but the hook's own output (the actual error from the migration tool / script) is never shown.

The output is also not recoverable after the fact from any other source:

Source Has hook output?
docker compose up (attached) ✅ streamed, prefixed svc-1 -> |
docker compose up -d ❌ (only <svc> hook exited with status N on failure)
docker compose up --wait ❌ same as -d
docker compose logs / docker logs <ctr>
docker inspect <ctr>
docker events payload
dockerd logs (journalctl -u docker)

This makes lifecycle hooks effectively undebuggable for any tool or CI pipeline that shells out docker compose up -d (the standard non-interactive invocation). When a post_start migration/seed/warm-up step fails, the operator sees hook exited with status 1 and has no way to find out why.

Related but distinct issues (hooks not running in certain paths): #13593 (compose run), #13318 (compose restart), #12513 (service.Up API). This report is about hooks that do run, but whose output goes nowhere.

Steps To Reproduce

compose.yaml:

yaml
services:
  ok:
    image: alpine:3.20
    command: sh -c "sleep 600"
    post_start:
      - command: sh -c "echo HOOK_STDOUT; echo HOOK_STDERR 1>&2; exit 0"
  bad:
    image: alpine:3.20
    command: sh -c "sleep 600"
    post_start:
      - command: sh -c "echo WHY_IT_FAILED; exit 7"
  1. Attached — output is visible:

    $ docker compose up --no-color
    ...
    ok-1 ->  | HOOK_STDOUT
    ok-1 ->  | HOOK_STDERR
    bad-1 ->  | WHY_IT_FAILED
    bad hook exited with status 7
  2. Detached — output is gone:

    $ docker compose up -d ; echo "exit=$?"
    ...
     Container proj-ok-1  Started
    bad hook exited with status 7
    exit=1

    HOOK_STDOUT, HOOK_STDERR, and WHY_IT_FAILED are nowhere. Same with --wait.

  3. Nothing recovers it afterwards:

    $ docker compose logs            # main container output only
    $ docker logs proj-bad-1         # main container output only
    $ docker inspect proj-bad-1      # no hook output / exit code field

Also observed: a failing post_start hook does not mark the container unhealthy (health check keeps passing, FailingStreak=0), so --wait cannot surface the failure via health state either — the only signal is the exit code of up itself.

Expected behavior

A hook that runs and fails should leave its output somewhere retrievable. At minimum one of:

  1. When a hook exits non-zero, its stdout/stderr is included in the CLI output alongside the existing <service> hook exited with status <N> line (this is the actual bug — the failure is reported without the reason).
  2. Hook output is written to a retrievable location — e.g. surfaced through docker compose logs for the owning service, or an ephemeral hook container whose logs survive like a failed pre_start hook's do.
  3. up -d / up --wait forward post_start / pre_stop hook stdout/stderr to the CLI (prefixed like attached mode), so a wrapping process can capture it.

Compose Version

Docker Compose version v5.1.2

Docker Environment

Docker Engine 29.4.0
OS: macOS (Darwin 25.6.0)

Anything else?

No response