Ask Docker where a container’s log is and it will tell you. Then, under most setups, it tells you nothing at all.
Here is the command:
docker inspect --format '{{.LogPath}}' my-container
On a stock Linux install that prints a path like this:
/var/lib/docker/containers/4f1c…e9a2/4f1c…e9a2-json.log
An empty line instead means one of the cases below. Check which logging driver the container uses first:
docker inspect --format '{{.HostConfig.LogConfig.Type}}' my-container
| Driver | Log file | LogPath shows it? |
|---|---|---|
json-file (the default) |
/var/lib/docker/containers/<id>/<id>-json.log |
Yes |
local |
/var/lib/docker/containers/<id>/local-logs/container.log |
No |
journald |
None. The lines go into the systemd journal | No |
syslog, gelf, fluentd and the rest |
None you should read. A local cache of up to 100 MB sits at /var/lib/docker/containers/<id>/container-cached.log |
No |
none |
Nothing is written anywhere | No |
The local driver leaves LogPath empty on purpose. Docker’s source says so: publishing the path would turn the file format into an API it has to keep stable.
Do not cat a local log or the cache file. Both are binary: length-prefixed protobuf records, with older files gzipped. Read them with docker logs.
Under journald, read the journal instead:
journalctl CONTAINER_NAME=my-container
The container never started. Docker fills in LogPath when the container starts. A container that was created and never run has no path, even under json-file.
You are on Docker Desktop. Mac and Windows run the engine inside a Linux VM. The path LogPath prints is inside that VM’s disk, not on your Mac. ls on the host finds nothing. Use docker logs.
/var/lib/docker is only the default data root. Two setups move it.
~/.local/share/docker, so the logs are under ~/.local/share/docker/containers/<id>/.data-root in /etc/docker/daemon.json moves everything, logs included. docker info --format '{{.DockerRootDir}}' prints where it went.LogPath always prints the real location, so trust it over any path you remember.
One JSON object per line:
{"log":"GET /health 200\n","stream":"stdout","time":"2026-10-04T17:02:11.123456789Z"}
log is the text, newline included. stream says stdout or stderr. time is when Docker received the line, in UTC.
Lines longer than 16 KiB are split across several entries. Only the last piece ends in \n.
That detail matters when you read the file with jq. Use -j, not -r:
sudo jq -j '.log' "$(docker inspect --format '{{.LogPath}}' my-container)"
-r adds a newline after every entry. Each line comes out double-spaced, and every split line comes out in pieces. -j adds nothing, so the newlines already in log do the work.
Only stderr:
sudo jq -j 'select(.stream=="stderr") | .log' "$(docker inspect --format '{{.LogPath}}' my-container)"
Read the file. Do not edit it. Docker keeps its own count of the file’s size, and changing the file underneath it confuses rotation. Limiting Docker log size covers what goes wrong.
This looks right and loses half the output:
docker logs my-container > my-container.log
docker logs keeps the container’s two streams apart. Its stdout goes to your stdout and its stderr goes to your stderr. Most applications log errors to stderr, so the redirect above saves everything except the errors.
Send both:
docker logs my-container > my-container.log 2>&1
Or keep them apart on purpose:
docker logs my-container > stdout.log 2> stderr.log
A container started with -t is the exception. A TTY merges the two streams before Docker sees them, so everything arrives on stdout.
Narrow it before you save it:
docker logs --since 24h --timestamps my-container > my-container.log 2>&1
docker logs --since 2026-10-04T09:00:00 --until 2026-10-04T10:00:00 my-container > incident.log 2>&1
--since and --until take a duration like 30m or 3h, a date, or an RFC 3339 timestamp. A timestamp with no offset is read in your local time zone. --timestamps prefixes each line with the time Docker received it.
Compose behaves differently, and here it is the friendlier one:
docker compose logs --no-log-prefix api > api.log
docker compose logs merges stderr into stdout itself, so a plain > keeps both. It drops the colours when the output is not a terminal. --no-log-prefix drops the api-1 | in front of each line, which you want in a file of one service and do not want in a file of several.
docker rm deletes the container’s directory, and the log goes with it. So does docker compose down, and so does a docker compose up -d that recreates a container after a config change.
Save first if you might need it:
docker logs my-container > my-container.log 2>&1 && docker rm my-container
A saved file answers today’s question. The next incident is in a container that was redeployed an hour before anyone looked.
The fix is to keep a copy that does not belong to the container. Switch the logging driver to journald and docker logs keeps working. Each line also lands in the journal, tagged with CONTAINER_NAME. Then ship the journal with the CL Agent, and every container’s history survives the container.
Sending Docker container logs to a central server walks through the driver change, the Compose syntax, and the queries worth running once the logs land.
json-file default never rotates, and the settings that fix it only apply to new containers.docker logs flags, Compose, and the three reasons it returns nothing.💌 Get notified on new features and updates