docker logs is the first command I reach for when anything goes wrong with a container, and it’s also the one I see used incorrectly most often — people either don’t know about the flags that make it actually useful (--since, --tail, -f), or they don’t understand why logs sometimes disappear entirely after a container restarts. This guide covers the command properly: from the absolute basics to the internals of how Docker captures and stores log output, plus the configuration decisions that determine whether docker logs works at all.
The Basics
docker logs <container_name_or_id>
Example — start a container that emits output on a loop:
docker run -d --name demo-app alpine sh -c "i=0; while true; do echo \"log line \$i\"; i=\$((i+1)); sleep 1; done"
Fetch its logs:
docker logs demo-app
Expected output:
log line 0
log line 1
log line 2
...
Following Logs in Real Time
Add -f (or --follow) to stream new log lines as they’re written, similar to tail -f:
docker logs -f demo-app
This keeps your terminal attached until you Ctrl+C. It does not stop the container — it just detaches your view.
Limiting Output With --tail
For a container that’s been running for hours, dumping the entire log history is rarely useful. Show just the last N lines:
docker logs --tail 20 demo-app
Expected output: the most recent 20 lines only, in order.
Combine with -f to follow starting from the last 20 lines instead of the beginning:
docker logs -f --tail 20 demo-app
Filtering by Time
Two flags control time-based filtering:
--since— show logs after a given time--until— show logs before a given time
Both accept RFC3339 timestamps, Unix timestamps, or relative durations like 10m, 1h.
docker logs --since 5m demo-app
Expected output: only lines logged in the last 5 minutes.
docker logs --since "2026-07-29T10:00:00" --until "2026-07-29T10:15:00" demo-app
Expected output: lines logged only within that 15-minute window — extremely useful when correlating logs to a specific incident window from a monitoring alert.
Including Timestamps
By default, docker logs doesn’t print timestamps. Add -t (or --timestamps):
docker logs -t --tail 5 demo-app
Expected output:
2026-07-29T10:22:01.123456789Z log line 118
2026-07-29T10:22:02.123456789Z log line 119
2026-07-29T10:22:03.123456789Z log line 120
2026-07-29T10:22:04.123456789Z log line 121
2026-07-29T10:22:05.123456789Z log line 122
This is stored internally regardless — Docker always timestamps log entries at capture time — -t just controls whether it’s printed.
Separating stdout and stderr
By default docker logs interleaves both streams. To isolate one:
docker logs demo-app 1>stdout.log 2>stderr.log
This works because Docker’s CLI writes stdout and stderr from the container to the CLI’s own stdout/stderr respectively, so normal shell redirection separates them into files.
Getting Logs From a Docker Compose Service
docker compose logs
docker compose logs -f web
docker compose logs --tail 50 --since 10m worker
Compose’s logs command is effectively a wrapper that aggregates docker logs across all containers in the project, prefixing each line with the service name:
web_1 | 200 GET /health
worker_1 | processing job 4821
Why Logs Sometimes Disappear: Logging Drivers
This is the part that trips people up most. docker logs doesn’t read from the container process directly — it reads from whatever logging driver is configured, which defaults to json-file. That driver writes each container’s stdout/stderr to a JSON file on the host at:
/var/lib/docker/containers/<container-id>/<container-id>-json.log
Check the current driver:
docker info --format '{{.LoggingDriver}}'
Expected output:
json-file
Inspect the raw file directly (useful when docker logs itself is misbehaving):
sudo cat /var/lib/docker/containers/$(docker inspect --format '{{.Id}}' demo-app)/*-json.log
Expected output — raw JSON, one object per line:
{"log":"log line 130\n","stream":"stdout","time":"2026-07-29T10:25:10.123456789Z"}
Critically: if you switch the logging driver to something like syslog, journald, fluentd, awslogs, gelf, or none, docker logs may return nothing or an error, because those drivers don’t support Docker’s log-reading API the same way (some, like journald, do support it; others like awslogs do not). This is by far the most common cause of “why is docker logs empty” support questions.
Check per-container driver override:
docker inspect demo-app --format '{{.HostConfig.LogConfig.Type}}'
If this returns none, that container was explicitly configured to discard logs — set at run time:
docker run -d --log-driver none alpine echo "this won't be captured by docker logs"
Configuring Log Rotation (a Common Production Gotcha)
By default, json-file logs grow unbounded unless you configure rotation — a chatty container can fill your disk. Set size limits and rotation count at run time:
docker run -d --name demo-app \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
alpine sh -c "while true; do echo tick; sleep 1; done"
Verify:
docker inspect demo-app --format '{{json .HostConfig.LogConfig}}'
Expected output:
{"Type":"json-file","Config":{"max-file":"3","max-size":"10m"}}
This caps total log storage per container at 30MB (10MB × 3 files), discarding the oldest once rotated. For a daemon-wide default (applies to all new containers), edit /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
sudo systemctl restart docker
Logs After Container Removal
Once a container is removed (docker rm), its log file is deleted along with it — docker logs on a removed container returns an error:
docker rm -f demo-app
docker logs demo-app
Expected output:
Error response from daemon: No such container: demo-app
This is why production systems ship logs to an external aggregator (see the Logspout guide) rather than relying on docker logs as long-term storage — it’s a live debugging tool, not a retention system.
Combining Flags for Real Debugging Scenarios
Investigating a crash that happened around 14:32, want context before and after, with timestamps:
docker logs -t --since "2026-07-29T14:30:00" --until "2026-07-29T14:35:00" demo-app
Watching a freshly deployed container for errors in real time, ignoring history:
docker logs -f --since 0s demo-app
Grepping logs for a specific error pattern:
docker logs demo-app 2>&1 | grep -i "error\|exception\|panic"
Security Considerations
- Log files under
/var/lib/docker/containers/are readable by root (and thedockergroup, which is effectively root-equivalent) — treat any secrets accidentally printed to stdout as compromised the moment they’re logged. - Avoid logging credentials, tokens, or PII from application code;
docker logsoutput is often shipped verbatim to third-party aggregation services. - Restrict
dockergroup membership on shared hosts — anyone in it can read every container’s logs, not just their own.
Troubleshooting Reference
| Symptom | Cause | Fix |
|---|---|---|
docker logs returns nothing | Logging driver doesn’t support log API (e.g. awslogs, gcplogs) or driver set to none | Check docker inspect --format '{{.HostConfig.LogConfig.Type}}'; view logs in the target system instead |
| Logs stop appearing mid-stream | Log file rotated/deleted, or disk full | Check df -h, configure max-size/max-file |
docker logs -f hangs forever with no new output | Container isn’t producing output, or app buffers stdout (not flushing) | Check app’s stdout buffering; for Python use python -u, for Node ensure no custom buffered writer |
| Old logs missing after container recreation | New container = new log file; old one deleted with old container | Ship logs externally before container replacement (CI/CD rolling deploys) |
permission denied reading log file directly | Not running as root/docker group | Use sudo, or stick to the docker logs CLI which handles permissions internally |
Real-World Deployment Notes
On any host running more than a handful of containers, I set max-size/max-file defaults in daemon.json on day one — it’s the single most common “why is my disk full” incident I’ve seen with Docker in production, and it’s entirely preventable with two lines of config. For teams already running a log aggregator (ELK, Loki, a SaaS platform), docker logs becomes primarily a local, immediate-feedback tool used during active debugging or in CI pipelines where spinning up a full logging stack isn’t worth it — docker compose logs -f during a test run is often all you need. Where I’ve seen docker logs genuinely fail teams is when they treat it as their only logging strategy in production: it’s tied to container lifecycle and host disk space, neither of which are guarantees you want your incident postmortems depending on.
Summary
docker logs looks like a trivial command until you need it under pressure — then the flags for time filtering, tailing, and stream separation become the difference between finding the root cause in 30 seconds and scrolling through thousands of irrelevant lines. Understanding that it’s backed by a configurable logging driver (not some magic container-internal buffer) explains almost every “why don’t my logs show up” issue you’ll hit, and configuring rotation up front saves you from a full disk during an incident, which is the worst possible time to discover you never set max-size.