
Fifteen years building engineering platforms, currently focused on advanced AI infrastructure at yeet. I love turning the deeply complex topics into something everyone can understand. I relate deeply with the core yeet philosophy that you can just build things.
Last updated: October 2026
Quick answer. Almost always because the application is not writing to stdout or stderr, which is the only thing
docker logscaptures. An app logging to a file inside the container produces an emptydocker logsforever, and a process whose output is block-buffered looks identical until it flushes. Check withdocker exec <container> tail -f /var/log/app.logbefore blaming the command. If the logs do exist and you just want to read them properly,docker logs --tail 100 -f <container>is the command:--taildefaults toall, so plain-freplays the container's entire history before showing anything current.
I build eBPF tooling at yeet, which means most container questions reach me after the obvious tooling has already come up short. docker logs is a good command with a narrow contract, and nearly all the frustration with it comes from expecting it to be a general view of what a container is doing rather than a view of one stream.
docker logs empty when my container is running?Usually because the application is not writing to stdout or stderr at all, and those two streams are the only thing docker logs captures. The container can be perfectly healthy, doing exactly what it should, and still produce an empty log forever.
This is the single most common cause and it is a design property rather than a failure. The daemon captures the standard output and standard error of the container's main process. An application configured to write to /var/log/app.log inside the container writes to a file in that container's filesystem, and the daemon never sees it. The container is working, the log file is filling up, and docker logs correctly reports that nothing was written to the streams it watches.
The usual fixes, in order of how often they apply:
/dev/stdout or a console appender. This is the convention container images are built around.docker exec -it <container> tail -f /var/log/app.log works when the image has a shell, and is the fastest way to confirm the application is logging at all.-u flag or PYTHONUNBUFFERED=1 is the common case.Buffering deserves its own mention because it looks exactly like no output at all. The logs are not missing, they are sitting in a buffer that has not been flushed, and the container will emit them later or never depending on how it exits.
Observe what the container actually did, rather than what it chose to print. This is a different measurement instead of a cleverer way to parse the same stream, which is why it works in exactly the cases where no amount of better log tooling helps.
Logs are voluntary. A container only tells you what its author decided to write down, so the failure mode that produces an empty log is the same one that produces a log without the detail you need. Kernel-level observation has no such dependency, because a syscall happens whether or not anything was logged about it.
curl -fsSL https://yeet.cx | sh # install yeet, once
yeet run gh:yeet-src/container-traffic # per-container bytes, TCP and UDP
yeet run gh:yeet-src/exectop # every process the container starts
These answer questions docker logs structurally cannot: which remote hosts a container is talking to, how much it is sending, and what it executed. Nothing is installed into the container, no application change is involved, and it works on an image you did not build and cannot modify.
This complements docker logs rather than replacing it. When an application logs well, its own messages are the best account of its intent. The kernel view is what you reach for when the log is silent, when you suspect the log is incomplete, or when the question is about behaviour nobody thought to log.
docker logs and follow?Combine --tail with -f, because each flag on its own does half of what people usually mean by tailing a log, and the half it leaves out is the annoying half:
docker logs --tail 100 -f my-container
--tail N limits output to the last N lines, and its default is all, which is why docker logs -f on a long-running container dumps months of history before it reaches anything current. -f (--follow) keeps the stream open and prints new lines as they arrive. The combination gives you recent context and then live output, which is the behaviour people expect from tail -f on a file.
The flags, confirmed against Docker 29.4.0:
| Flag | What it does |
|---|---|
-n, --tail string | Lines from the end of the logs. Default all |
-f, --follow | Follow log output |
-t, --timestamps | Prefix each line with a timestamp |
--since string | Logs since a timestamp or relative time, such as 42m |
--until string | Logs before a timestamp or relative time |
--details | Show extra attributes supplied to the logging driver |
--tail takes a string rather than a number because all is a legal value, so --tail all -f is valid and means exactly the same thing as plain -f. That is also the quiet reason the default surprises people: the flag is documented as a string with a default of all, which reads as a sensible default until you run it against a container that has been up for three months.
docker logs -f and tail -f?They look alike, they are spelled similarly, and they read from completely different places. The distinction stays invisible while everything works and becomes the whole problem the moment a log rotates or a driver changes.
tail -f follows a file on a filesystem, and it keeps following that inode. docker logs -f asks the Docker daemon to stream a container's captured output, and the daemon is reading from whatever logging driver that container was configured with. There is not necessarily a file involved at all.
Three practical consequences follow from that difference:
tail -f can end up holding a rotated-away file, which is why tail -F exists. docker logs -f has no such problem because the daemon owns the handle, though rotation can still discard history you wanted.tail -f the log file and expect a supported path. The JSON files under /var/lib/docker/containers/<id>/ are an implementation detail rather than an interface, and reading them directly breaks when the driver changes.docker logs can read from a remote driver. When logs are shipped to Splunk or Fluentd, the daemon keeps a local cache so docker logs still works, which a file-based tool could not do.docker logs say the logging driver does not support reading?Because the container uses a remote logging driver and the local cache that normally backs it up has been switched off. Both halves have to be true, which is why this error is rarer than the number of people running remote drivers would suggest.
The message is Error response from daemon: configured logging driver does not support reading, and it appears for drivers such as splunk, fluentd and none that ship logs elsewhere rather than storing them locally. Docker's answer to this is dual logging: the engine keeps a local cache alongside the remote driver so docker logs keeps working, and it enables that automatically when the configured driver cannot read.
So seeing this error generally means someone opted out deliberately, by setting "cache-disabled": "true" in daemon.json, usually to save disk on hosts shipping everything to a central collector. Removing that setting restores local reads at the cost of that disk. The exception is the none driver, where there is nothing to read by design and no setting will change it.
Checking which driver a container actually uses takes one command:
docker inspect --format '{{.HostConfig.LogConfig.Type}}' my-container
docker logs from a container that already exited?docker logs works perfectly well on stopped containers, which is why reflexively cleaning one up is the mistake that costs the most during an incident. The logs survive the process exiting and they do not survive the container being deleted, so the window for recovering them closes the moment someone tidies up.
A container that exited still has its logs until the container is removed, so docker logs <id> on a stopped container returns its full output. docker ps -a lists the stopped ones along with their exit codes. What destroys the evidence is docker rm, or starting the container with --rm, which removes it on exit and takes the logs with it.
docker ps -a --filter "status=exited" --format '{{.ID}} {{.Names}} {{.Status}}'
docker logs --tail 200 -t <container-id>
The -t flag matters more here than it does during live tailing. When you are watching output arrive you already know roughly when each line appeared, but when reconstructing a crash afterwards the ordering of the last few lines relative to the exit time is usually the exact thing you are trying to establish.
Not with docker logs itself, which accepts exactly one container name and has no multi-container mode. The realistic options are a shell loop over the running set, Compose when it already defines the group you care about, or stepping outside log reading entirely.
For a handful of containers, backgrounding one follow per container and prefixing each line with its name works well enough and needs nothing installed beyond what you already have:
for c in $(docker ps --format '{{.Names}}'); do
docker logs --tail 20 -f "$c" 2>&1 | sed "s/^/[$c] /" &
done
wait
Under Compose, docker compose logs -f --tail 20 does the same job properly across every service in the project, with names already prefixed and no stray background jobs to clean up. Whenever Compose is what defines the set of containers, it is the better answer.
Both approaches share the limitation that started this post. They aggregate stdout and stderr across a larger number of containers, which is useful, and neither can show you anything the applications never printed in the first place.
docker logsAn empty docker logs almost never means an idle container. The command's contract is the stdout and stderr of one process, so an application writing to a file inside the container, or buffering its output, produces nothing here while working perfectly. Check those two things and the container's logging driver before concluding anything is wrong. When the logs do exist, docker logs --tail 100 -f is the command to reach for, because --tail defaults to all and plain -f replays the entire history first.
docker logs --tail 50 my-container. Add -f to keep following after those 50 lines, and -t to prefix timestamps.
docker logs --tail?all, which is why docker logs -f on a long-running container replays its entire history before showing current output. Pass --tail explicitly to bound it.
docker logs in real time?docker logs -f <container>, usually with --tail so it does not replay everything first. The stream stays open until you interrupt it or the container stops.
docker logs with a timestamp filter?Yes. --since and --until accept an RFC 3339 timestamp or a relative value such as 42m, so docker logs --since 10m -t my-container bounds the window to the last ten minutes.
Most often output buffering in the application, since a process whose stdout is a pipe tends to block-buffer. Setting PYTHONUNBUFFERED=1 or the equivalent for your runtime makes output appear as it is written.
With the default json-file driver, under /var/lib/docker/containers/<container-id>/. That path is an implementation detail rather than a supported interface, and it changes with the logging driver, so prefer docker logs.
There is no docker logs --clear. Recreating the container is the supported route; truncating the JSON file on disk works with the default driver but is unsupported and can confuse the daemon.
docker logs work after a container stops?Yes, until the container is removed. A container started with --rm or deleted with docker rm takes its logs with it, so avoid --rm on anything whose failure you may need to investigate.
Configure rotation on the driver, with max-size and max-file options on the container or as daemon-wide defaults in daemon.json. Without them the default json-file driver grows without bound.
docker logs show logs written to a file inside the container?No. It captures the main process's stdout and stderr only. Use docker exec <container> tail -f /path/to/log to read a file inside the container, or configure the application to log to stdout.
docker container logs CLI reference, for the flag set, verified against Docker 29.4.0cache-disabled settingcontainer-traffic, per-container byte accounting at the socket layerexectop, process launches per container