At 2 a.m., a payment container starts returning errors. You run docker logs payments, expecting the last few minutes of requests and stack traces, but get an empty response, or one line from hours earlier. The application is clearly doing something, yet the command gives you almost nothing useful.
That failure usually isn't mysterious. Docker captures a container's standard output and standard error, then hands those streams to a logging driver. The default json-file driver stores the records locally as JSON lines, so visibility depends on where the application writes, which driver the container uses, and how much history remains on disk. Docker's docker container logs reference documents the command and its default logging behavior.
Table of Contents
- Why Docker Container Logs Are Harder Than They Look
- Viewing Container Logs with docker logs and docker-compose logs
- Rotating the Default json-file Driver Without Filling the Disk
- Choosing and Configuring a Logging Driver
- The Blocking vs Non-Blocking Trade-Off Nobody Warns You About
- Shipping Logs to a Centralized Platform with Fluxtail
Why Docker Container Logs Are Harder Than They Look
Docker container logs aren't the same thing as kernel logs, systemd service logs, or arbitrary files inside a container. Docker's capture path starts with stdout and stderr. If an application writes only to /var/log/app.log inside its own filesystem, docker logs won't see that file unless the application redirects the stream or another collector reads the file directly.
The default json-file driver stores captured messages as JSON records containing the message, timestamp, and stream metadata. That representation works well enough for local inspection, but it creates operational surprises. The file is managed under Docker's data directory, commonly beneath /var/lib/docker/containers, rather than appearing as a normal application log that your shell tools can interpret directly.
Practical rule: Before changing a logging driver, confirm where the process writes. A perfectly configured driver can't collect bytes the application never sends to
stdoutorstderr.
Several production failure modes follow from that design:
- Unbounded local growth: The default file-based approach can consume the host's root volume when rotation isn't configured. This turns verbose application output into a Docker or host stability incident.
- Awkward text processing: The on-disk file contains JSON wrappers, so running
grepagainst the raw file can produce escaped content and metadata rather than clean messages. - Fragmented exceptions: Multi-line stack traces may arrive as separate records. A search for one complete trace can miss the relationship between its lines.
- Misleading history limits:
--tail,--since, and--untilcan only filter records the selected driver still has available. They can't recover entries already rotated, deleted, or never captured. - Empty output: A container may be healthy from Docker's perspective while its application writes somewhere outside the captured streams.
A useful mental model is that docker logs is a driver-backed stream reader, not a universal filesystem search tool. For broader log-reading patterns, the Fluxtail guide to reading logs is a useful companion, but the first diagnostic remains local: inspect the container's output contract, logging driver, and retained history.
Viewing Container Logs with docker logs and docker-compose logs
Start with the smallest command:
docker logs payments
This fetches the records available through the container's configured logging driver. If the service is noisy, constrain the result before following it:
docker logs --tail 200 payments
docker logs --follow payments
docker logs -f --tail 200 payments
--tail 200 asks Docker for the most recent retained records, while --follow keeps the command attached as new records arrive. Combining them is safer during an incident than opening an unrestricted historical stream in a busy terminal.
Timestamps make copied output easier to compare with alerts and traces:
docker logs --timestamps payments
docker logs --since 30m --until 5m payments
The timestamp flag adds Docker's timestamp prefix to each record. --since and --until narrow the query window, but they don't expand retention. If the driver has already rotated away the relevant period, the command can't return it.
Docker can also expose extra engine-provided details when the driver supports them:
docker logs --details payments
For a quick error search, combine Docker's filtering with ordinary shell tools:
docker logs --timestamps nginx 2>&1 | grep '5xx'
The 2>&1 matters when you're piping output in a shell and want both command streams handled consistently. For a focused incident artifact, redirect the result:
docker logs --timestamps payments > incident.log
That file is useful for sharing, but treat it as a snapshot, not a durable archive.

Compose output adds service context
With Docker Compose, the service name is usually more useful than an individual container name:
docker-compose logs -f --tail 50
docker-compose logs -f --tail 50 api
docker-compose logs --timestamps --no-log-prefix api
docker-compose logs prefixes records with the Compose service, which helps when several services are streaming into one terminal. If you need output that another parser can consume, --no-log-prefix removes that extra service label. Keep --timestamps when you need to align output across services.
For a practical Compose-focused command reference, see how to use Docker Compose logs. When output is unexpectedly empty, check the service's driver and application destination before assuming Compose lost the records.
Rotating the Default json-file Driver Without Filling the Disk
The default json-file driver is convenient because docker logs can read it directly, but convenience without limits creates host-level risk. Docker stores the records in its container data area, commonly using a path shaped like /var/lib/docker/containers/<id>/<id>-json.log. The exact root can vary with the Docker installation, so inspect the container rather than hard-coding the location.
A daemon-wide configuration establishes a baseline for newly created containers:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "5"
}
}
Place that configuration in /etc/docker/daemon.json, validate the file before applying it, and restart the Docker daemon during a controlled maintenance window. Existing containers may retain their current logging configuration, so verify and recreate workloads when necessary.
A per-container override keeps the decision close to the workload:
docker run \
--log-driver=json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
payments:latest
Compose expresses the same choice under the service:
services:
payments:
image: payments:latest
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Rotation settings need an ownership decision
| Setting | Docker default | Recommended | What changes |
|---|---|---|---|
| Driver | json-file |
json-file with explicit limits, or a centralized driver |
Defines the local capture and delivery path |
max-size |
No operational limit should be assumed | A tested size appropriate to the workload | Rotates the active file when it reaches the configured size |
max-file |
No bounded retention should be assumed | A deliberately chosen file count | Bounds how many rotated files remain locally |
| Scope | Daemon default | Daemon baseline plus service exceptions | Determines whether recreated containers inherit the policy |
Rotation controls local disk exposure, not total observability. A short local retention period can be acceptable when a central platform has confirmed delivery, but it becomes dangerous if the central sink is the only copy and the forwarding path drops records.
Also watch the lifecycle detail that catches teams during long incidents. Rotation doesn't make old data magically durable, and a long-running process can keep an active handle open while the driver manages successive files. The safe operational objective is a bounded file count and an observed disk budget, not merely a configured size value.
Choosing and Configuring a Logging Driver
The logging driver is an architectural choice, not a cosmetic Docker setting. Docker documents built-in options including syslog, journald, gelf, fluentd, awslogs, and splunk, alongside local file-based approaches. Its logging plugin documentation describes drivers as a way to forward container logs to another service for processing.
The right choice depends on what must happen when the destination is unavailable. Local drivers keep inspection close to the host. Network-oriented drivers simplify central collection, but they introduce transport latency, reachability, buffering, and failure behavior that you must test.
| Driver | Buffering | Best fit |
|---|---|---|
json-file |
Local Docker-managed files | Simple local inspection with docker logs |
local |
Local Docker-managed storage | Host-local retention with Docker's local format |
syslog |
Syslog transport and receiver behavior | Existing syslog infrastructure |
journald |
Systemd journal handling | Hosts already standardized on journald |
gelf |
GELF transport behavior | Collectors that accept structured or legacy GELF records |
fluentd |
Fluentd forwarder and its buffers | Routing, enrichment, and collector pipelines |
splunk |
Splunk-oriented forwarding | Environments already operating Splunk ingestion |
awslogs |
AWS logging service path | Workloads tied to AWS log collection |
gcplogs |
Google Cloud logging path | Workloads tied to Google Cloud collection |
Daemon defaults and workload exceptions
A daemon-level setting in /etc/docker/daemon.json gives containers a common default:
{
"log-driver": "local"
}
That is operationally consistent, but it affects every container created under that daemon. A per-container setting is narrower:
docker run \
--name payments \
--log-driver=local \
--log-opt max-size=10m \
--log-opt max-file=3 \
payments:latest
The important distinction appears during recreation. A daemon default applies to newly created containers, while a per-container override is recorded with that workload's configuration. In practice, declare the setting in Compose, Terraform, or another source-controlled definition so a replacement doesn't revert to an unsuitable default.
Driver options aren't interchangeable. max-size and max-file are meaningful for local file rotation, while remote drivers use transport-specific keys such as an address, tag, endpoint, or asynchronous delivery option. Check the selected driver's accepted options before copying a configuration from another driver.
The Blocking vs Non-Blocking Trade-Off Nobody Warns You About
A logging pipeline can protect the application from a slow collector, or it can protect the log record from being discarded. It can't promise both without a deliberately designed buffer and recovery path.
In blocking mode, the container's output path waits for the logging driver to accept the record. If a network sink slows down or becomes unreachable, the application can stall while trying to write logs. That may preserve more records, but it can also turn an observability outage into an application outage.
In non-blocking mode, Docker accepts output into an in-memory buffer and a separate delivery path sends records to the driver. This keeps the application moving when the sink has backpressure, but the buffer is finite. Once it fills, new records can be lost. Independent operational guidance recommends considering non-blocking delivery for network-based drivers, while also warning that its loss behavior must be understood rather than treated as a free performance improvement. See Docker logging gotchas and delivery trade-offs.

Define the loss budget before setting the buffer
A useful planning model is:
buffer capacity divided by expected log rate equals the approximate drop window
Use consistent units, and calculate from observed workload behavior rather than a guess. If a service emits bursts, use its burst rate for the safety calculation, not its quiet average. Then test the result by slowing or interrupting the collector and observing what the application and driver do.
A configuration might look like this for a driver that supports non-blocking delivery:
docker run \
--log-driver=fluentd \
--log-opt fluentd-address=collector:24224 \
--log-opt mode=non-blocking \
--log-opt max-buffer-size=8m \
payments:latest
The option names and support vary by driver, so validate them against the selected driver's documentation. Don't assume mode=non-blocking means durable queueing. An in-memory buffer is not a disk-backed retry log.
Check Docker's warning path and operational signals for dropped records. docker inspect can expose warning information associated with the container, but the exact visibility depends on the driver and Docker version. Your runbook should record the test method, the induced sink failure, the observed application latency, and any drop indication. If nobody can prove what happened during collector downtime, the pipeline doesn't have a defensible loss policy.
Audit workloads need a different answer: For security, financial, or compliance records, application continuity may not justify silent loss. Blocking, local durable buffering, or a separate audit pipeline can be safer than a large in-memory queue.
Use this rubric:
- Low loss tolerance: Prefer a durable local or collector-side buffer, and accept controlled backpressure where required.
- High application availability priority: Use non-blocking delivery, but alert on degraded delivery and document the acceptable loss window.
- Uncertain sink latency: Measure it under outage conditions before choosing a mode.
- Incident-heavy service: Preserve enough local context to investigate delivery failures, not just application failures.
Shipping Logs to a Centralized Platform with Fluxtail
Centralized shipping starts with a clear contract. Decide which fields every record needs, how the service and environment are named, and which transport fits the application. Fluxtail's Docker container logs use case supports centralizing container stdout and stderr while retaining operational context such as service, container, host, environment, level, and request fields.
For applications that already emit structured JSON, send JSON records over HTTP. The exact endpoint and authentication values belong in your Fluxtail workspace configuration, so keep them in secrets rather than committing them to Compose files:
curl -X POST "$FLUXTAIL_INGEST_URL" \
-H "Authorization: Bearer $FLUXTAIL_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @record.json
A record should carry the original event time and stable identity fields:
{
"timestamp": "2026-08-20T02:14:31Z",
"level": "error",
"service": "payments",
"environment": "production",
"container_id": "container-id",
"host": "docker-host",
"image": "payments:latest",
"message": "upstream authorization failed",
"request_id": "request-id"
}
The HTTP path is appropriate when the application or collector already produces structured events. It avoids reconstructing fields from a plain text message, but it puts responsibility on the sender to handle authentication, timeouts, retries, and duplicate delivery safely.
GELF works well for plain-text applications
For workloads that write ordinary lines to stdout or stderr, Docker's GELF driver can send records to a GELF receiver:
services:
legacy-api:
image: legacy-api:latest
logging:
driver: gelf
options:
gelf-address: "udp://fluxtail-gelf:12201"
tag: "legacy-api"
UDP keeps the application path lightweight, but it doesn't provide the same delivery guarantees as a durable queue. Use it when the accepted loss policy matches the transport, and verify that the receiver is reachable from the Docker host.
Put a forwarder between Docker and HTTPS
A Fluentd forwarder gives you a place to enrich, route, buffer, and apply delivery controls before sending records over HTTPS. Docker can emit to the local forwarder, while Fluentd handles the external destination:
services:
api:
image: payments:latest
logging:
driver: fluentd
options:
fluentd-address: "fluentd:24224"
tag: "docker.payments"
mode: "non-blocking"
max-buffer-size: "8m"
fluentd:
image: fluent/fluentd:latest
ports:
- "24224:24224"
volumes:
- ./fluentd.conf:/fluentd/etc/fluent.conf:ro
A configuration fragment can accept Docker's forward protocol, add geographical context when the record contains a usable client address, and fail outbound requests rather than waiting indefinitely:
<source>
@type forward
port 24224
bind 0.0.0.0
</source>
<filter docker.**>
@type geoip
geoip_database "/fluentd/etc/GeoLite2-City.mmdb"
skip_adding_null_record true
backend_library geoip2_c
<record>
client_country ${record["geoip"]["country"]["iso_code"]}
client_city ${record["geoip"]["city"]["names"]["en"]}
</record>
</filter>
<match docker.**>
@type http
endpoint "#{ENV['FLUXTAIL_INGEST_URL']}"
headers {"Authorization":"Bearer #{ENV['FLUXTAIL_TOKEN']}"}
open_timeout 5s
read_timeout 10s
<buffer>
@type file
path /fluentd/buffer/fluxtail
flush_interval 5s
retry_forever false
</buffer>
</match>
Treat that fragment as an integration pattern, then validate the exact Fluentd plugin, record schema, authentication method, and endpoint expected by your Fluxtail workspace. A hard timeout prevents a stuck destination from holding resources forever, while a file buffer gives the forwarder a recovery mechanism that an in-memory Docker buffer cannot provide by itself.
Centralized hygiene beats clever parsing: Emit structured fields at the application boundary, keep service and environment tags consistent, propagate timestamps in UTC, and verify the resulting record rather than trusting the configuration.
After shipping, query Fluxtail for a known test event and confirm that container_id, host, and image are present on every ingested record. Also verify that the service, environment, severity, and request fields remain searchable after enrichment. A pipeline is ready for incident use only when you can trace one event from the container stream through the driver, forwarder, and central record.
Fluxtail centralizes Docker stdout and stderr over protocol-based ingest paths, with live tailing, filtering, alerts, and searchable context for incident response. Visit Fluxtail to connect your container logging path and verify that the fields you need remain available from first capture through investigation.