Docker & ContainersXVII Β· LoggingCentral collection
Central log collection β Fluent Bit, Vector, Loki
What you'll learn
- Compare driver-push, file-tail and API-read collection topologies
- Explain which topology couples application availability to the log pipeline
- Configure Fluent Bit or Vector against a Docker host, with state that survives a restart
- Recognise the rotation race between the shipper and the logging driver
- Verify end to end that a line emitted on the host arrives in the store
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12
Per-host logs answer βwhat did this container doβ. Central logs answer βwhy did the order failβ, which is the question that spans six services and three hosts and is the only one anybody asks during an incident.
Getting there is not one architecture. It is three, and they differ in exactly the property that matters when things break: what happens to your application when the log destination is unavailable.
The three topologies
flowchart LR
subgraph A["A - driver push"]
C1[Container] --> D1[Daemon + remote driver] --> S1[(Store)]
end
subgraph B["B - agent tails files"]
C2[Container] --> D2[Daemon json-file] --> F2["/var/lib/docker/containers"] --> AG2[Fluent Bit] --> S2[(Store)]
end
subgraph C["C - agent reads the API"]
C3[Container] --> D3[Daemon any driver] --> API["/containers/id/logs"] --> AG3[Vector] --> S3[(Store)]
end
| A: driver push | B: agent tails files | C: agent reads API | |
|---|---|---|---|
| Driver required | fluentd, gelf, syslog, β¦ | json-file only | any readable driver |
| Store outage blocks the app | Yes, by default | No | No |
| Local copy retained | dual-logging cache | the json files | the driver store |
| Extra process per host | none | one agent | one agent |
| Metadata available | driver-attached | parsed from path | full container object |
| Survives agent restart | n/a | with a position DB | with the driver store |
A β the driver pushes
The daemonβs logging driver connects directly to the aggregator. No agent, no files, fewest moving parts.
It is also the topology that makes your log aggregator a synchronous
dependency of every container on the host. Delivery mode defaults to
blocking, so when the aggregator is unreachable the containerβs write to
stdout does not complete and the application stalls. mode=non-blocking with
a bounded max-buffer-size mitigates it by dropping lines instead.
Choose A when you have very tight host resource constraints and you have
consciously accepted the coupling and set mode=non-blocking.
B β an agent tails the JSON files
The daemon writes json-file as usual. A Fluent Bit or Vector process on the
host reads /var/lib/docker/containers/*/*.log, parses, enriches, and ships.
The container never touches the network path. If the store is down, the agent buffers or stalls and the application does not notice. This is the default recommendation for a reason.
C β an agent reads the Docker API
Vectorβs docker_logs source retrieves logs through the Docker API rather
than reading files directly. That makes it driver-agnostic: it works with
json-file, local, journald, and β via the dual-logging cache β with
remote drivers too.
It also gets the full container object for free, so container_name, image,
and container labels arrive as structured fields rather than being
reconstructed from a filesystem path. The cost is a socket dependency: the
agent needs access to the Docker API.
Fluent Bit against a Docker host
The keys below are the ones that make the difference between a demo and something that survives a restart:
[SERVICE]
Flush 5
Daemon off
Log_Level info
storage.path /var/lib/fluent-bit/storage
storage.sync normal
storage.backlog.mem_limit 64M
[INPUT]
Name tail
Path /var/lib/docker/containers/*/*-json.log
Parser docker
Tag docker.*
DB /var/lib/fluent-bit/tail.db
Docker_Mode On
Skip_Long_Lines On
Refresh_Interval 5
Rotate_Wait 30
Mem_Buf_Limit 32M
storage.type filesystem
[FILTER]
Name record_modifier
Match docker.*
Record host ${HOSTNAME}
[OUTPUT]
Name loki
Match *
Host loki.example.com
Port 3100
labels job=docker, host=${HOSTNAME}
label_keys $container_name
line_format json
Four of those keys are the ones people leave out and then have an incident about:
DBpersists the inode-to-offset mapping in a SQLite file. Without it, an agent restart re-reads every log file from the beginning, duplicating weeks of records into the store, or from the end, losing everything written while it was down. Neither is acceptable and the default is no DB at all.Rotate_Wait(default5seconds) is how long the agent keeps watching a file after it has been rotated, to drain what is still buffered. Five seconds is not enough on a busy host; see the race below.Docker_Mode(defaultfalse) reassembles log lines that Docker split because they exceeded its internal read buffer. Without it, a long JSON log line arrives as several fragments, none of which parses.Skip_Long_Lines(defaultfalse) makes the agent skip a line longer than its buffer instead of stopping. Off means one pathological line can wedge the input.
Note what is not in that config: there is no kubernetes filter. That
filter enriches records by querying a Kubernetes API server for pod metadata,
and on a plain Docker host there is no API server to query. It is a common
copy-paste from a Kubernetes example and it does nothing useful here.
Vector against a Docker host
[sources.docker]
type = "docker_logs"
auto_partial_merge = true
exclude_containers = ["vector"]
[transforms.parse]
type = "remap"
inputs = ["docker"]
source = '''
. = parse_json(string!(.message)) ?? { "message": .message }
.container_name = .container_name
'''
[sinks.loki]
type = "loki"
inputs = ["parse"]
endpoint = "http://loki.example.com:3100"
labels = { job = "docker", host = "{{ host }}", container = "{{ container_name }}" }
[sinks.loki.encoding]
codec = "json"
[sinks.loki.buffer]
type = "disk"
max_size = 536870912
when_full = "block"
exclude_containers keeps the agent from ingesting its own output, which is
otherwise a genuine feedback loop: the agent logs a delivery failure, reads
its own failure message, tries to deliver it, fails, logs that. On a store
outage this produces exponential growth of purely self-referential records.
auto_partial_merge (default true) is Vectorβs equivalent of Fluent Bitβs
Docker_Mode: Docker splits log messages that exceed 16 KB, and this
reassembles them.
The rotation race
This is the failure specific to topology B, and it is the reason lines go missing from the store while every component reports healthy.
CONTAINER=api
CID=$(docker inspect --format '{{.Id}}' "$CONTAINER")
LOGFILE="/var/lib/docker/containers/$CID/$CID-json.log"
A=$(sudo stat -c %s "$LOGFILE"); sleep 10; B=$(sudo stat -c %s "$LOGFILE")
RATE=$(( (B - A) / 10 ))
SIZE_MB=$(docker inspect --format '{{index .HostConfig.LogConfig.Config "max-size"}}' "$CONTAINER")
FILES=$(docker inspect --format '{{index .HostConfig.LogConfig.Config "max-file"}}' "$CONTAINER")
echo "rate=$RATE bytes/s max-size=$SIZE_MB max-file=$FILES"
echo "buffer seconds = (max-size bytes * max-file) / $RATE"Choosing the store
| Store | Indexing model | Strength | Watch out for |
|---|---|---|---|
| Loki | Labels only; log body unindexed | Cheapest at volume; native Grafana | Label cardinality |
| Elasticsearch / OpenSearch | Full text | Arbitrary queries, aggregations | Cost and cluster operations |
| Splunk | Full text | Compliance tooling, mature RBAC | Licensing by ingest volume |
| CloudWatch Logs | Limited | Zero operations on AWS | Query ergonomics, per-GB cost |
| Datadog / SaaS | Full text | No infrastructure | Ingest-based pricing surprises |
For a team standing something up today: Loki and Grafana. It is free, it operates simply, and its cost model matches container logging β you pay for bytes stored, not for indexing every field of every line.
Verification that can fail
Configuration audits prove nothing about a pipeline. Emit a unique marker on the host and confirm it arrives at the far end.
MARKER="probe-$(hostname -s)-$(date +%s)"
docker run --rm alpine:3.20 echo "$MARKER"
# Checkpoint 1: did the daemon capture it? (json-file / local / journald)
sudo grep -rl --include='*-json.log' -F "$MARKER" /var/lib/docker/containers/ \
&& echo 'OK: daemon captured the line' \
|| echo 'FAIL: driver did not write it locally'
# Checkpoint 2: did the shipper read it?
docker logs --since 2m fluent-bit 2>&1 | tail -20
# Checkpoint 3: query the store. Adjust for your Loki endpoint.
LOKI=http://loki.example.com:3100
curl -sG "$LOKI/loki/api/v1/query_range" \
--data-urlencode 'query={job="docker"}' \
--data-urlencode "start=$(( $(date +%s) - 300 ))000000000" \
| grep -q -F "$MARKER" \
&& echo 'PASS: marker reached the store' \
|| echo 'FAIL: marker not in the store'Run it from cron on one host per rack and alert when it fails. A logging pipeline that nobody probes is a pipeline you discover is broken on the day you need it β and the evidence you needed was never collected, so there is no recovering it retrospectively.
Also watch the shipperβs own metrics, because βit is runningβ is not βit is deliveringβ:
$ curl -s http://127.0.0.1:2020/api/v1/metrics | python3 -m json.tool{
"input": {
"tail.0": { "records": 184203, "bytes": 71223104, "files_opened": 62, "files_rotated": 41 }
},
"output": {
"loki.0": { "proc_records": 183940, "errors": 0, "retries": 12, "retries_failed": 0 }
}
}Illustrative output
The number to alert on is retries_failed. Nonzero means records were
dropped after exhausting the retry budget: they existed, the agent read them,
and they are now gone. records minus proc_records growing steadily means
the agent is falling behind, which is the leading indicator of the rotation
race above.
Knowledge check
Knowledge check Β· 5 questions
Q1. You run Fluent Bit tailing /var/lib/docker/containers/*/*-json.log. A colleague switches the daemon to the local driver to get rotation by default. What happens?
Q2. Which Loki label would you refuse to add for a Docker fleet?
Q3. Why does the Fluent Bit tail input need a DB setting on a production host?
Q4. Which topologies keep the application running when the central store is unreachable? Select all that apply.
Q5. Even with logs shipped to a central store, the per-host log copy still needs a size ceiling.
Passing score: 75%. Answers are checked in this browser.