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=1
HOOK_STDOUT, HOOK_STDERR, and WHY_IT_FAILED are 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 logs for the owning service, or an ephemeral hook container whose logs survive like a failed pre_start hook's do.
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
Description
Service lifecycle hooks (
post_start,pre_stop) run their command, but their stdout/stderr is only forwarded to the terminal whendocker compose upruns attached (foreground). In every non-interactive form —up -d,up --wait— the hook output is dropped:<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:
docker compose up(attached)svc-1 -> |docker compose up -d<svc> hook exited with status Non failure)docker compose up --wait-ddocker compose logs/docker logs <ctr>docker inspect <ctr>docker eventspayloadjournalctl -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_startmigration/seed/warm-up step fails, the operator seeshook exited with status 1and 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.UpAPI). This report is about hooks that do run, but whose output goes nowhere.Steps To Reproduce
compose.yaml:Attached — output is visible:
Detached — output is gone:
HOOK_STDOUT,HOOK_STDERR, andWHY_IT_FAILEDare nowhere. Same with--wait.Nothing recovers it afterwards:
Also observed: a failing
post_starthook does not mark the container unhealthy (health check keeps passing,FailingStreak=0), so--waitcannot surface the failure via health state either — the only signal is the exit code ofupitself.Expected behavior
A hook that runs and fails should leave its output somewhere retrievable. At minimum one of:
<service> hook exited with status <N>line (this is the actual bug — the failure is reported without the reason).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 Environment
Anything else?
No response