Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[Bug]: container logs -f exits immediately with no output when the container's log file is empty

Open Beginner friendly
#2,374 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@i098 is already working on this.

Since Oct 11, 2026.

  • #2377 by @i098 — open

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
swift
Domain
cli

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() returns nil for a zero-byte file — see read(upToCount:), which maps length == 0 to nil in 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 linked tail() code.
  • The early return sits before the follow branch (ContainerLogs.swift#L105-L109), so -f never reaches followFile.
  • followFile is already built for an empty start — its first act is seekToEnd() (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 -f until the file becomes non-empty) fixes the symptom for one caller but adds a busy-wait to the CLI, does not help consumers of the ContainerClient.logs API who tail the file themselves, and still leaves the flag failing to honor its contract.
  • Server-side (block the containerLogs XPC 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 --follow as: "The docker logs --follow command will continue streaming the new output from the container's STDOUT and STDERR" (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 -f on a container whose log file is currently empty blocks, streams lines as they are written, and exits only when the stream ends (repro above: hello appears after 10s)
  • container logs without -f on an empty log is unchanged (no output, exit 0)
  • container logs -f on 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

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from apple/container

All issues in apple/container

Similar issues

More Swift issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.