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

Open

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

  1. self-assigned this
    on Sep 10, 2026
  2. glours commented on Sep 10, 2026

    @glours
    Contributor

    Thanks for the detailed report! The main issue you flagged, a failing hook returning only an exit code with no explanation, has been fixed in v5.5.1. Hooks now capture stdout and stderr, even in detached mode, and failure messages include the tail of that output, prioritizing stderr.

    Could you upgrade and confirm that this resolves the issue?

    Successful post_start and pre_stop hooks are still silent in detached mode, and their output can’t be retrieved through docker compose logs or docker logs. This is because hooks run through docker exec, separately from the container’s log stream. If either limitation matters for your use case, we can keep this issue open and focus it on those two points.

  3. maks-oleksyuk commented on Sep 10, 2026

    @maks-oleksyuk
    Author

    Thanks—upgraded to v5.5.1 and confirmed the failure message now carries the hook output. Tested on a real server: a post_start hook doing exit 7 after writing to stderr now reports bad hook exited with status 7: REASON_ON_STDERR with a non-zero exit, so the reason reaches the deployment log. That covers the main case from the report.

    Two things still make hooks hard to work with if you want to keep the issue open scoped to them:

    1. On failure only a short tail is shown, not the full run. With a multi-step hook script, the earlier output is lost. Example on v5.5.1 a post_start hook that prints 25 step N done lines to stdout, then FATAL: db connection refused to stderr, then exit 3:
    script hook exited with status 3: FATAL: db connection refused
    

    The 25 progress lines are gone, and nothing (docker compose logs, docker logs, docker inspect) has them. So for anything more than a one-liner, you still can't tell how far it got or what the earlier steps printed.

    1. Successful hooks are completely silent in detached mode. A hook that runs fine produces no output at all under up -d / up --wait - no way to confirm what it did (rows seeded, migrations applied, cache warmed). Since hooks run through docker exec, docker compose logs never carry their output either.

    Ideal outcome for CI/PaaS wrappers: the hook stdout/stderr is forwarded to the CLI in detached mode as it happens (prefixed like attached mode does with svc-1 -> |), or is otherwise retrievable after the run—regardless of whether the hook succeeded or failed.

  4. ndeloof commented on Sep 12, 2026

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions