[Bug]: container logs -f exits immediately with no output when the container's log file is empty
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
Research direction
The bug is in tail() in Sources/ContainerCommands/Container/ContainerLogs.swift: the early return on a nil readToEnd() result runs before the follow branch. Start by reproducing with the minimal container run -d plus container logs -f sequence from the issue. Done when -f on a zero-byte log blocks and streams later writes, while non-follow mode and non-empty -f behave as before.
Written by the indexing model from the issue text.
Description
Problem
container logs -f exits immediately — no output, no follow, exit code 0 — when the container's log file is still empty at call time. A caller that starts tailing right after container run -d (the normal pattern for a supervised dev service) silently gets nothing, and tooling that treats "the log tail exited" as "the task is done" can go on to tear down a perfectly healthy container.
Real-world impact
A Taskfile-based dev stack ran container run -d --rm --name postgres -e ... -v postgres:/var/lib/postgresql -p 5432:5432 postgres:18-alpine, then tailed with container logs -f postgres, with defer: container stop postgres registered for Ctrl+C cleanup. With a freshly auto-created volume, postgres writes nothing to its log while the container VM boots and initdb runs; the tail hit the zero-byte log and exited within the same second (the deferred container stop fired immediately after, with no log lines in between, and no error reported — i.e. exit 0). The result: postgres was killed ~1s after coming up.
Evidence that postgres did come up in the "failed" run — from the next run's recovery log on the same volume (the volume had been deleted before the failed run, so the timestamp must be from that run):
2026-10-09 15:03:08.142 UTC [27] LOG: database system was interrupted; last known up at 2026-10-09 14:27:23 UTC
2026-10-09 15:03:08.223 UTC [27] LOG: database system was not properly shut down; automatic recovery in progress
14:27:23 is the failed run (stack start 14:27:21, container start step completed at +2s). Re-running the same task dev against the now-initialized volume works: the entrypoint's first line ("Skipping initialization") reaches the log long before initdb would on a fresh volume, the tail's initial read finds it, prints, and follows normally. The only difference between the failing and succeeding runs is whether the volume was fresh.
Minimal repro
container run -d --rm --name logempty alpine sh -c 'sleep 10; echo hello'
sleep 0.5
time container logs -f logempty; echo "exit=$?"
container stop logempty
Expected per the code path below: container logs -f returns within a second, prints nothing, exit=0 — even though the container is still running and emits hello 10s later. Expected per the flag's contract ("Follow log output"): block, then stream hello when it appears.
Root cause
container logs reads the whole log file before ever considering follow:
- The full-file read in
tail(): ContainerLogs.swift#L89-L95 —guard let data = try fh.readToEnd() else { return }, with the in-code comment noting the nil case is a zero-byte read. FileHandle.readToEnd()returnsnilfor a zero-byte file — seeread(upToCount:), which mapslength == 0tonilin the open-source mirror of Foundation (FileHandle.swift#L504-L515); the same nil-on-empty behavior on Darwin Foundation is documented by the comment in the linkedtail()code.- The early
returnsits before the follow branch (ContainerLogs.swift#L105-L109), so-fnever reachesfollowFile. followFileis already built for an empty start — its first act isseekToEnd()(ContainerLogs.swift#L112-L113) — so nothing downstream prevents following an empty log.- The server side just hands back read handles to the on-disk log files (ContainersService.swift#L754-L755), so this is a purely client-side sequencing bug.
Not a duplicate: #1967 and #2022 are about -n line truncation, #2009 is about the on-disk log freezing after an attached client dies. This is about -f not following at all when the file starts empty.
Code evidence is against main at 48f902dc (2026-10-08); the real-world impact was observed on macOS with a current container CLI — the code path above is deterministic for any build containing the linked tail() implementation.
Proposed solution
Make the early return apply only to non-follow mode, so -f falls through to followFile even when the log is empty:
} else if !follow {
// Fast path if all they want is the full file.
guard let data = try fh.readToEnd() else {
return
}
guard let str = String(data: data, encoding: .utf8) else {
throw ContainerizationError(
.internalError,
message: "failed to convert container logs to utf8"
)
}
print(str.trimmingCharacters(in: .newlines))
}
fflush(stdout)
if follow {
setbuf(stdout, nil)
try await Self.followFile(fh: fh)
}
Why this over the alternatives:
- CLI-side retry/poll (re-invoke
logs -funtil the file becomes non-empty) fixes the symptom for one caller but adds a busy-wait to the CLI, does not help consumers of theContainerClient.logsAPI who tail the file themselves, and still leaves the flag failing to honor its contract. - Server-side (block the
containerLogsXPC response until the first byte arrives) would change the semantics of every log retrieval — including non-follow, where an empty log legitimately means "nothing yet" — and adds latency where the on-disk file is already the natural stream source. - Document as working-as-intended contradicts the flag's own help text ("Follow log output") and the direction of #1752, which asks for "follow/stream behavior that works reliably for orchestrators reading multiple containers concurrently." Docker's equivalent documents
--followas: "Thedocker logs --followcommand will continue streaming the new output from the container'sSTDOUTandSTDERR" (Docker CLI reference).
Behavioral delta: container logs -f on an empty log will now block and stream instead of exiting 0. Blocking on empty input is the standard -f/tail -f contract, and the follow stream already ends when the log file is truncated or the seek fails (ContainerLogs.swift#L116-L125), so a stopped/removed container still ends the stream.
Acceptance criteria
-
container logs -fon a container whose log file is currently empty blocks, streams lines as they are written, and exits only when the stream ends (repro above:helloappears after 10s) -
container logswithout-fon an empty log is unchanged (no output, exit 0) -
container logs -fon a non-empty log is unchanged (prints existing content, then follows) - Regression test: follow on a zero-byte log file streams subsequent writes (unit or CLITest)
- Dominant language
- Swift
- Stars
- 50.5k
- Forks
- 1.8k
- Avg merge
- 4d 8h
- Merged PRs (30d)
- 22
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing 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.
More from apple/container
-
[Request]: Improve robustness and path resolution in ensure-hawkeye.shPossibly taken @denizguney claimed this 8 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
docs: k8s CNI with Cilium example in docs/kubernetes.mdPossibly taken @zdk claimed this 10 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
[Bug]: kernel download hangs forever when the server stops sendingPossibly taken @devops-thiago claimed this 11 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Maintainers usually reply within 1 day
-
[Bug]: container export -o deletes an existing directoryPossibly taken @devops-thiago claimed this 11 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
[Bug]: --mount and -v accept a relative container path and mount it at /<path>Possibly taken @devops-thiago claimed this 11 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 82/100
521xueweihan/HelloGitHub#3940 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
macOS button text differs from SwiftUI in interaction snapshotsPossibly taken @Kyle-Ye claimed this today. Openarea: rendering area: text impact: visual-diff platform: macOS type: bug
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
OpenSwiftUIProject/OpenSwiftUI#1140 ·
Maintainers usually reply within 1 day
-
Local host row takes the name of an exe.dev VM when /etc/hosts aliases that VM to a loopback addressOpen
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
SRGSSR/pillarbox-apple#1604 · 1 comment ·
Maintainers usually reply within 1 day