Skip to content
Blog

Tail and Follow Container Logs with docker logs

docker logs -f, --tail, --since and timestamps, plus the json-file vs local drivers, log rotation, and how to stop losing container output.

Published on • October 31, 2026

AI Assistant

docker logs is the first command you reach for when a container misbehaves — and the first one that betrays you when the daemon’s logging driver was never configured. This post covers the command itself (following, tailing, filtering by time), then the machinery underneath it: which driver stores your output, why it silently grows until the disk fills, and how to make sure the lines you need are still there tomorrow.

The basics: docker logs reads STDOUT and STDERR

Whatever your container’s main process writes to STDOUT and STDERR is captured by the configured logging driver and readable with docker logs. It does not read files the application writes inside the container — if your app logs to /var/log/app.log, Docker will happily show you nothing. The official nginx image works around this by symlinking its log files to /dev/stdout and /dev/stderr; do the same for your own services, or configure the app to log to the console.

Follow: -f

$ docker logs -f api

--follow keeps the stream open and prints new lines as they arrive, like tail -f. It exits on Ctrl-C without affecting the container. Combine it with --tail to get context and live updates:

$ docker logs -f --tail 100 api

This is the single most useful debugging invocation in Docker: the last 100 lines of history, then everything that happens next.

Tail: -n / --tail

$ docker logs --tail 50 api          # last 50 lines
$ docker logs -n 20 api

The default is all — the entire retained log. On a chatty container that has been up for weeks, docker logs api with no --tail means waiting while Docker reads (and decompresses) everything. Always pass --tail in scripts. A negative or non-integer value is invalid and falls back to all.

Time filters: --since, --until, --timestamps

--since and --until accept RFC 3339 timestamps, Unix timestamps, or Go duration strings:

$ docker logs --since 30m api                 # last half hour
$ docker logs --since 2026-10-31T09:00:00+07:00 api
$ docker logs --since 1h --until 30m api      # the window between
$ docker logs -f --since 5m api

To correlate lines with external events (deploys, alerts, user reports), add --timestamps:

$ docker logs --timestamps --tail 20 api
2026-10-31T08:12:01.123456789Z starting up
2026-10-31T08:12:01.987654321Z listening on :8080

Timestamps are RFC 3339 with nanosecond precision, which makes grep-and-sort workflows on exported logs reliable. --details adds driver-provided attributes (labels and environment variables passed to --log-opt).

A realistic debugging session combines all of it:

$ docker ps -a                              # find the container and its exit code
$ docker logs --tail 100 exited-job         # what happened at the end
$ docker logs --since 10m --timestamps api | grep -i error
$ docker logs -f --tail 50 api              # watch it fail live

What’s actually storing your logs: json-file vs local

The docker logs command is only a reader. Writing is done by a logging driver, one per container, chosen at creation time. Docker’s default is json-file, which appends each line to a JSON file under /var/lib/docker/containers/<id>/<id>-json.log.

Check your daemon’s default:

$ docker info --format '{{.LoggingDriver}}'
json-file

The important difference between the two local drivers:

json-file (default)local
FormatOne JSON object per lineCustom, more compact format
max-size defaultUnlimited (-1)20m
max-file default1 (only effective when max-size is set)5
Compression of rotated filesNoYes, by default
docker logs supportYesYes

Docker’s own documentation tips you toward local: it performs log rotation by default and uses a more efficient format, while json-file is kept as the default for backwards compatibility (and because Kubernetes uses Docker as its runtime).

Why your logs disappear — or never stop growing

Two opposite failures, same root cause: nobody configured rotation.

Unbounded growth. With default json-file, max-size is unlimited. A service that logs 10 MB an hour writes ~240 MB/day into /var/lib/docker/containers/, forever, until the daemon’s disk fills — and a full disk takes down everything else on the host, not just Docker. Set limits per container:

$ docker run -d --log-driver json-file \
    --log-opt max-size=10m --log-opt max-file=3 \
    --name api my-api:latest

Or globally in /etc/docker/daemon.json (values must be strings):

{
  "log-driver": "local",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Restart the daemon (sudo systemctl restart docker, or docker desktop restart on Docker Desktop) — and remember: the change only affects newly created containers. Existing containers keep the options they were created with; to update one, recreate it. Check what a container actually got:

$ docker inspect -f '{{.HostConfig.LogConfig}}' api
map[max-file:3 max-size:10m Type:json-file]

Rotation deletes old lines. The flip side of max-file: 3 with max-size: 10m is that at most ~30 MB of history exists per container. If the crash you’re investigating happened hours ago and the service is noisy, those lines are gone. That’s a feature for disk safety and a trap for post-mortems: ship logs off the host (a syslog, fluentd, journald, awslogs driver, or an agent reading the files) when you need retention, and keep docker logs as the short-term buffer.

With a remote driver, docker logs still works thanks to dual logging: the Engine keeps a local-driver cache (default 5 files × 20 MB, compressed) when the configured driver can’t be read. You can disable it with --log-opt cache-disabled=true if you only read logs remotely — but then docker logs returns configured logging driver does not support reading.

Compose vs raw docker logs

docker compose logs reads from the same drivers but works across the whole project and uses service names:

$ docker compose logs -f --tail 100 web
$ docker compose logs --since 1h api worker
$ docker compose logs -f            # every service in the project

Differences to keep straight:

  • docker logs targets a container by name or ID; docker compose logs targets services by their name in the Compose file, and follows the containers it creates when you compose up again.
  • Rotation and driver selection are still per-container — set them in the Compose file so they survive re-creation:
services:
  api:
    image: my-api:latest
    logging:
      driver: local
      options:
        max-size: "10m"
        max-file: "5"
  • If a container was recreated, docker compose logs shows output from the current containers; old output lives with the old, removed containers — which is exactly why rotation limits should be set before you need them.

Further reading