`post_start` / `pre_stop` hook stdout/stderr is discarded in `up -d` and `up --wait` — only attached mode shows it, and nothing persists it
@glours is already working on this.
Since Sep 10, 2026.
- Dominant language
- Go
- Stars
- 38.2k
- Forks
- 5.8k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 55
Description
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"
-
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 -
Detached — output is gone:
$ docker compose up -d ; echo "exit=$?" ... Container proj-ok-1 Started bad hook exited with status 7 exit=1HOOK_STDOUT,HOOK_STDERR, andWHY_IT_FAILEDare nowhere. Same with--wait. -
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:
- 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). - Hook output is written to a retrievable location — e.g. surfaced through
docker compose logsfor the owning service, or an ephemeral hook container whose logs survive like a failedpre_starthook's do. up -d/up --waitforwardpost_start/pre_stophook 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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.