Skip to content

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

Description

@maks-oleksyuk

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:

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions