Docker Logging Driver Tuning: Defaults, Formulas, and Disk-Safe Configuration

35 min read

Published

No vague advice. Every logging option: what it does, the default if you omit it, what goes wrong, and exactly how to calculate the right value — with numbers.

Most Docker guides say "set max-size so logs don't fill your disk." That's true, but useless at 3 a.m. when /var/lib/docker is at 98% and you don't know whether to change daemon.json, Compose, or both.

This guide is different. For every important logging setting you'll get four things:

  1. What it does — in plain language
  2. Default value — what Docker uses if you write nothing
  3. What goes wrong — if you leave the default or set it wrong
  4. How to calculate — step-by-step with real numbers

We'll use a concrete example server throughout:

Example server (our reference case):
4 vCPU · 8 GB RAM · 80 GB root disk · 12 containers in one Compose stack · moderate logging (API + Nginx + workers) · peak ~45 MB/day total log output across the stack

Adjust the numbers for your server. The method stays the same.

How to read this guide (start here)

Every code block has a colored label. Do only what the label says.

Follow parts in order unless a part says you can skip it. Change example IPs, passwords, and paths to match your lab.

Suggested path: Parts 1–2 (read) → pick a driver (3 or 4) → Part 6 walkthrough → Part 8 templates.

Part 1 — How Docker logging works

What this part is for: understand where container stdout goes and why disks fill up.
What you should do: read only.

When your app writes to stdout or stderr, Docker's logging driver captures it. You don't mount a log volume unless you choose to — Docker handles storage on the host.

Default driver: json-file

Unless you change it globally or per-container, Docker uses the json-file driver. Each log line is wrapped in JSON with timestamp and stream metadata, then appended to a file on the host.

Log path pattern: /var/lib/docker/containers/<container-id>/<container-id>-json.log
Container ID starts with a1b2c3d4e5f6...
Log file: /var/lib/docker/containers/a1b2c3d4e5f6.../a1b2c3d4e5f6...-json.log
Find it quickly: docker inspect --format='{{.LogPath}}' my-container

Critical fact: With default json-file settings, there is no size limit. One chatty container can grow a single log file until your root disk is full. This is the #1 cause of "Docker server ran out of space overnight."

Data flow (mental model)

READ — explanatory content. Follow the paragraph above.
App process → stdout/stderr → Docker container runtime → logging driver → host disk (or remote sink)

json-file  → /var/lib/docker/containers/.../...-json.log
local      → /var/lib/docker/containers/.../...-local/logs/ (ring buffer)
syslog     → syslog daemon on host or remote
journald   → systemd journal
fluentd    → Fluentd collector → Elasticsearch/S3/etc.
awslogs    → Amazon CloudWatch Logs

Part 2 — Global daemon.json vs per-service Compose

What this part is for: cover: Global daemon.json vs per-service Compose.
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.

You can set logging in two places. They interact — and confusion here causes "I changed daemon.json but nothing happened."

/etc/docker/daemon.json (global default)

Applies to new containers that don't specify their own logging block. Does not retroactively change existing containers — you must recreate them.

COPY FILE — create the config file named in the section (edit values first).
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "compress": "true"
  }
}

After editing:

RUN — paste into a shell on the host this section describes.
# 1. Validate JSON syntax
python3 -m json.tool /etc/docker/daemon.json

# 2. Restart Docker (stops all containers unless live-restore is on — plan a window)
sudo systemctl restart docker

# 3. Confirm daemon picked up the config
docker info --format '{{.LoggingDriver}}'
docker info --format '{{json .Plugins.Log}}' | jq .

# 4. Recreate containers so they get new defaults (existing containers keep old LogConfig)
cd /opt/myapp
docker compose up -d --force-recreate

# 5. Verify one container
docker inspect --format='{{json .HostConfig.LogConfig}}' api | jq .

Sample output — docker inspect ... LogConfig after recreate

{
  "Type": "json-file",
  "Config": {
    "max-size": "10m",
    "max-file": "3",
    "compress": "true"
  }
}

Meaning: this container is using the json-file driver with rotation. If Config is {} or missing max-size, the container was created before the daemon/Compose change — recreate it. Type: "local" or "none" means a different driver; path and rotation rules differ. Values must be strings in Compose/daemon.json.

Important: Changing daemon.json does not update containers that are already running. You must recreate them. Also: every value in log-opts must be a string — write "3" not 3, and "true" not true.

docker-compose.yml (per-service override)

Per-service logging overrides the daemon default for that service only. This is how you give a noisy worker more headroom while keeping a quiet Redis container tight.

COPY FILE — create the config file named in the section (edit values first).
services:
  api:
    image: myapp/api:1.0.0
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"
        compress: "true"

  redis:
    image: redis:7-alpine
    logging:
      driver: json-file
      options:
        max-size: "5m"
        max-file: "2"

Which wins?

Precedence rule

What it does: Determines effective log settings when both daemon.json and Compose define options.

Default if omitted: Daemon default (json-file, unlimited size) applies when Compose has no logging block.

If you leave default: Every service without a Compose override grows logs without rotation. A 12-service stack on an 80 GB disk can fill up in days.

How to calculate: Set safe global defaults in daemon.json, then override only services that need more (or less) retention.

Global: max-size 10m, max-file 3 → 30 MB cap per container by default
Override api to 20m × 5 = 100 MB because it logs every request
Override redis to 5m × 2 = 10 MB because it barely logs

Part 3 — json-file driver options

What this part is for: cover: json-file driver options.
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
16 KB line limit: json-file truncates/splits lines longer than ~16 KB. Long stack traces may break across lines — prefer structured logging or a remote driver for huge payloads.

max-size

What it does: Maximum size of a single log file before Docker rotates it. Accepts suffixes: k, m, g (e.g. 10m, 100m).

Default if omitted: -1 (unlimited — no rotation by size)

If you leave default: Log file grows until disk full. df -h shows /var/lib/docker or root at 100%. Containers may fail to start; host can become unresponsive.

How to calculate — step by step:

  1. Measure one container's daily log output (see Part 9): e.g. API writes 30 MB/day
  2. Decide retention window per file set: e.g. keep 3 days of logs on disk
  3. Target total per container: 30 × 3 = 90 MB
  4. Pick max-size × max-file ≈ 90 MB. Example: max-size: 30m, max-file: 3 → 90 MB ✓
max-size ≈ daily_log_MB × retention_days ÷ max-file
API: 30 MB/day, 3 days retention, max-file 3 → max-size = 30 × 3 ÷ 3 = 30m
Quiet sidecar: 1 MB/day → max-size 5m, max-file 2 is plenty

max-file

What it does: Number of rotated log files to keep per container. When the limit is reached, the oldest file is deleted.

Default if omitted: 1

Critical: max-file only has effect when max-size is also set. Without max-size, there is no rotation — max-file alone does nothing useful.

If you leave default (with max-size set): Only one file exists at a time (the active file). When it rotates, the previous file is deleted immediately — you keep almost no history for debugging.

How to calculate:

max disk per container = max-size × max-file
max-size 10m, max-file 3 → 10 × 3 = 30 MB max on disk per container
max-size 50m, max-file 10 → 50 × 10 = 500 MB per container — fine for one service, dangerous × 20 containers

compress

What it does: When true, rotated log files are gzip-compressed (e.g. ...-json.log.1.gz). Saves disk; slightly more CPU on rotation.

Default if omitted: false

If you leave default: Rotated files use full uncompressed size. On text-heavy logs, compression often saves 70–90% on rolled files.

How to calculate: Enable when disk is tight. Effective disk ≈ max-size × max-file × (1 if active file + ~0.15–0.3 for compressed rolled files on typical app logs).

10m × 3 without compress ≈ 30 MB
10m × 3 with compress ≈ ~12–18 MB typical (active 10 MB + 2 rolled at ~2–4 MB each)
Set compress: "true" on production hosts with limited disk — almost always worth it.

labels and env (json-file only)

What they do: Attach container labels or named environment variables as extra JSON fields on each log line. Helps filtering when you later ship logs to Loki or Elasticsearch.

Default if omitted: Log lines contain only log, stream, and time fields.

Not the same as Docker labels on the container — you must list which label keys to include in labels: option.

labels: "com.docker.compose.service,com.docker.compose.project"
Never use env for secrets — only APP_ENV, REGION, etc.
Options that do not apply to json-file: tag, fluentd-address, syslog-address — those belong to remote logging drivers. Using tag under json-file is silently ignored or rejected depending on Engine version.

Part 4 — local driver (modern alternative)

What this part is for: cover: local driver (modern alternative).
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.

The local driver stores logs in a binary ring buffer — faster and more disk-efficient than json-file for high-volume logging. Human-readable via docker logs; on-disk format is not plain text.

local — max-size

What it does: Maximum size of each log file chunk before rotation inside the ring buffer.

Default if omitted: 20m

If you leave default: Usually reasonable. Very chatty apps may need lowering if total footprint matters, or raising if you need longer in-driver retention.

How to calculate: Same daily-growth method as json-file. local defaults to 20m per chunk — verify total footprint with du on the container log directory.

Chatty API at 50 MB/day → set max-size: "50m" or switch to centralized logging; default 20m rotates frequently but ring buffer caps total size

local — max-file

What it does: Maximum number of log file segments retained in the ring buffer.

Default if omitted: 5 (Docker Engine 20.10+)

If you leave default: Rough cap ≈ max-size × max-file before oldest data is overwritten — but ring semantics mean oldest log lines drop, not necessarily whole days.

How to calculate:

approx max disk per container (local) ≈ max-size × max-file
Defaults: 20m × 5 = ~100 MB per container
Our stack: 12 × 100 MB = 1.2 GB worst case with all defaults — acceptable on 80 GB disk
Tight disk: max-size: "10m", max-file: "3" → 30 MB per container → 360 MB stack total

When to pick local over json-file

COPY FILE — save as daemon.json (edit passwords/IPs first).
# daemon.json — switch global default to local
{
  "log-driver": "local",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Part 5 — Getting logs out of Docker (centralized logging)

What this part is for: cover: Getting logs out of Docker (centralized logging).
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.

Local rotation (max-size / max-file) solves disk safety. It does not solve search, retention, or alerts. For that you need logs outside the container host.

There are two production patterns. Most teams use one or combine both:

  1. Driver-based shipping — Docker sends each log line directly to a remote system (Fluentd, syslog, Loki plugin, CloudWatch). No log file on disk (or minimal buffering).
  2. Collector-based shipping — Keep json-file/local on disk with rotation; run Fluent Bit, Promtail, Filebeat, or Vector on the host to tail files and forward to a central store.
Which to pick?
Single host, small stack → json-file + rotation + Fluent Bit or Promtail is simple and debuggable.
Multi-host, compliance, long retention → central Loki, ELK, or CloudWatch with a collector on every node.
Zero local disk for logs → set logging driver to fluentd/syslog/loki and accept that docker logs depends on driver buffering.

5.1 — Method A: Docker logging driver → remote (direct ship)

The container runtime forwards stdout/stderr to the driver. Logs may never hit *-json.log on disk.

fluentd driver

What it does: Sends logs over Fluent Forward protocol to a Fluentd or Fluent Bit instance listening on TCP (default port 24224).

When to use: You already run Fluentd/Fluent Bit; you need flexible routing (parse, filter, send to S3, ES, Loki).

Key options: fluentd-address, tag, fluentd-async, fluentd-buffer-limit

What goes wrong: Collector down + fluentd-async=false → app blocks on log writes under load.

Step 1 — Run Fluent Bit as a collector on the host:

Production: Do not expose port 24224 on 0.0.0.0 to the internet. Bind to 127.0.0.1 or a private Docker network only; restrict with host firewall if published.
COPY FILE — save as /opt/fluent-bit/fluent-bit.conf (edit passwords/IPs first).
# /opt/fluent-bit/fluent-bit.conf
[SERVICE]
    Flush        1
    Log_Level    info

[INPUT]
    Name              forward
    Listen            0.0.0.0
    Port              24224

[OUTPUT]
    Name              loki
    Match             *
    Host              loki.example.com
    Port              3100
    Labels            job=docker,host=${HOSTNAME}
    auto_kubernetes_labels off
COPY FILE — save as docker-compose.yml (edit passwords/IPs first).
# docker-compose.yml for Fluent Bit
services:
  fluent-bit:
    image: fluent/fluent-bit:3.1
    container_name: fluent-bit
    restart: unless-stopped
    ports:
      - "24224:24224"
    volumes:
      - ./fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro
    network_mode: host   # or attach to same compose network as apps

Step 2 — Point your app at Fluent Bit:

COPY FILE — create the config file named in the section (edit values first).
services:
  api:
    image: myapp/api:1.0.0
    logging:
      driver: fluentd
      options:
        fluentd-address: "127.0.0.1:24224"
        tag: "docker.api.{{.Name}}"
        fluentd-async: "true"
        fluentd-buffer-limit: "1048576"

Step 3 — Verify:

RUN — paste into a shell on the host this section describes.
docker compose up -d fluent-bit api
docker logs api          # still works (driver buffers recent lines)
# Check Fluent Bit received data in its logs
docker logs fluent-bit

Sample output — Fluent Bit and application logging smoke test (excerpt)

[2026/08/30 16:20:11] [ info] [input:forward:forward.0] listening on 0.0.0.0:24224
[2026/08/30 16:20:13] [ info] [output:loki:loki.0] worker #0 started
[2026/08/30 16:20:18] [ info] [output:loki:loki.0] http_status=204

Meaning: the Forward input is listening, Loki worker started, and HTTP 204 confirms a batch was accepted. Connection-refused/retry lines mean collector-to-Loki reachability or credentials; no app records means its driver address/tag or container recreation is wrong.

syslog driver

What it does: Forwards logs to syslog (rsyslog on host or remote SIEM).

When to use: Enterprise syslog/SIEM (Splunk HEC via rsyslog, Graylog syslog input, traditional NOC).

Key options: syslog-address (tcp://host:514 or udp://), syslog-format (rfc3164/rfc5424), tag

COPY FILE — create the config file named in the section (edit values first).
services:
  api:
    logging:
      driver: syslog
      options:
        syslog-address: "tcp://logs.example.com:514"
        tag: "{{.Name}}"
        syslog-format: "rfc5424"

gelf driver (Graylog)

What it does: Sends GELF over UDP/TCP to Graylog.

Key options: gelf-address (udp://graylog:12201), tag, gelf-compression-level

COPY FILE — create the config file named in the section (edit values first).
services:
  api:
    logging:
      driver: gelf
      options:
        gelf-address: "udp://graylog:12201"
        tag: "api"

awslogs driver

What it does: Ships directly to Amazon CloudWatch Logs (ECS/EKS/EC2).

Key options: awslogs-group, awslogs-region, awslogs-stream-prefix, awslogs-create-group

Requires: IAM role on the instance/task with logs:CreateLogStream, logs:PutLogEvents.

COPY FILE — create the config file named in the section (edit values first).
services:
  api:
    logging:
      driver: awslogs
      options:
        awslogs-group: "/production/myapp"
        awslogs-region: "eu-west-1"
        awslogs-stream-prefix: "api"
        awslogs-create-group: "true"

Loki Docker plugin (grafana/loki-docker-driver)

What it does: Community plugin that pushes logs straight to Grafana Loki — no Fluent Bit in the middle.

When to use: Loki-only stack, minimal moving parts.

Install once per host:

Production: --grant-all-permissions is required by the Loki plugin installer but grants broad Docker API access. Install only from the official image; restrict who can manage plugins on the host.
RUN — paste into a shell on the host this section describes.
docker plugin install grafana/loki-docker-driver:3.0.0 \
  --alias loki \
  --grant-all-permissions

# Verify
docker plugin ls

Sample output — docker plugin ls after Loki driver install

ID             NAME          DESCRIPTION           ENABLED
f3a61c8016c2   loki:latest   Loki Logging Driver   true

Meaning: Docker registered the plugin under alias loki and it is enabled. false prevents new Loki-logged containers from starting; install/permission errors mean do not switch Compose to this driver yet.

COPY FILE — create the config file named in the section (edit values first).
services:
  api:
    logging:
      driver: loki
      options:
        loki-url: "http://loki:3100/loki/api/v1/push"
        loki-batch-size: "400"
        labels: "service,environment"
        loki-external-labels: "host={{.Hostname}},container={{.Name}}"

5.2 — Method B: json-file on disk + collector (most common on VPS)

Keep local rotation for emergency docker logs and disk safety. Run a collector that reads log files from /var/lib/docker/containers/.

Why teams prefer this: If Loki/Elasticsearch is down, logs still exist locally. Collectors can catch up after outages. Easier to debug with docker logs and raw files.

Option B1 — Promtail → Loki (Grafana stack)

COPY FILE — save as promtail-config.yml (edit passwords/IPs first).
# promtail-config.yml
server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s
    relabel_configs:
      - source_labels: ['__meta_docker_container_name']
        target_label: container
      - source_labels: ['__meta_docker_container_log_stream']
        target_label: stream
COPY FILE — create the config file named in the section (edit values first).
services:
  promtail:
    image: grafana/promtail:3.0
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./promtail-config.yml:/etc/promtail/config.yml:ro
    command: -config.file=/etc/promtail/config.yml
    networks:
      - monitoring

  loki:
    image: grafana/loki:3.0
    ports:
      - "3100:3100"
    networks:
      - monitoring

  grafana:
    image: grafana/grafana:11.2.0
    ports:
      - "3000:3000"
    networks:
      - monitoring

networks:
  monitoring:

Your app services keep normal json-file logging with max-size/max-file. Promtail ships independently — changing the app compose does not break shipping.

Option B2 — Fluent Bit tailing json logs

COPY FILE — save as fluent-bit.conf (edit passwords/IPs first).
# fluent-bit.conf — read json-file logs from disk
[INPUT]
    Name              tail
    Path              /var/lib/docker/containers/*/*-json.log
    Parser            docker
    Tag               docker.*
    Refresh_Interval  5
    Mem_Buf_Limit     10MB
    Skip_Long_Lines   On

[FILTER]
    Name              modify
    Match             docker.*
    Add               host ${HOSTNAME}

[OUTPUT]
    Name              es
    Match             docker.*
    Host              elasticsearch
    Port              9200
    Index             docker-logs
    Suppress_Type_Name On
COPY FILE — create the config file named in the section (edit values first).
services:
  fluent-bit:
    image: fluent/fluent-bit:3.1
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro
    network_mode: host

Option B3 — Filebeat → Elasticsearch (ELK)

COPY FILE — save as filebeat.yml (edit passwords/IPs first).
# filebeat.yml
filebeat.inputs:
  - type: container
    paths:
      - '/var/lib/docker/containers/*/*.log'
    processors:
      - add_docker_metadata:
          host: "unix:///var/run/docker.sock"

output.elasticsearch:
  hosts: ["https://elasticsearch:9200"]
  username: "elastic"
  password: "${ES_PASSWORD}"

5.3 — Method C: Hybrid (recommended production pattern)

What actually works on real servers:

  1. daemon.json — safe json-file defaults (10m × 3, compress) as a floor
  2. Compose overrides — per-service caps for noisy containers
  3. Collector on host — Promtail or Fluent Bit ships to Loki/ES
  4. Retention in Loki/ES — 30–90 days; local disk only keeps 2–3 days for emergencies
Disk budget split:
Local (json-file): 400 MB stack cap → survive collector outage 48h
Loki: 30 days retention → search, dashboards, alerts
Never rely on local files alone for compliance or incident review

5.4 — journald and none drivers

journald

What it does: Writes to systemd journal. Unified with system logs.

Read: journalctl CONTAINER_NAME=api -f

Tune host: /etc/systemd/journald.conf → SystemMaxUse=500M, MaxRetentionSec=7day

none

What it does: Discards all container logs.

When to use: Ephemeral CI runners, healthcheck-only sidecars, extreme disk constraints — never on production APIs without remote shipping.

COPY FILE — create the config file named in the section (edit values first).
services:
  healthcheck-sidecar:
    image: busybox
    logging:
      driver: "none"

Part 6 — Practical walkthrough: first production host from zero

What this part is for: configure logging on a production-like host.
What you should do: follow COPY → RUN → verify in order.

Follow this order on a fresh VPS. Skipping steps is how you end up with unlimited logs and no central search.

Step 1 — Measure before tuning (day 0)

RUN — paste into a shell on the host this section describes.
# Deploy stack with defaults, wait 24h, then:
sudo du -sh /var/lib/docker/containers/*/*-json.log | sort -hr | head -10
docker ps --format '{{.Names}}' | while read c; do
  echo -n "$c: "
  sudo stat -c%s "$(docker inspect --format='{{.LogPath}}' $c)" 2>/dev/null | awk '{print int($1/1048576)" MB"}'
done

Sample output — 24-hour container log-size survey (excerpt)

31M	/var/lib/docker/containers/a1.../a1...-json.log
9.8M	/var/lib/docker/containers/b2.../b2...-json.log
api: 31 MB
nginx: 9 MB
redis: 0 MB

Meaning: sorted file sizes identify the noisiest logs, then names map bytes back to services. Missing paths may indicate local/remote drivers; stat errors can mean a container stopped/recreated during the loop. Measure after a representative period before sizing caps.

Step 2 — Set daemon.json floor

Merge, don't overwrite: Back up /etc/docker/daemon.json first. A raw tee removes unrelated keys such as mtu (see MTU article).
RUN — merge logging defaults into daemon.json (maintenance window).
sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak.$(date +%F) 2>/dev/null || true
echo '{}' | sudo tee /etc/docker/daemon.json >/dev/null
sudo jq '. + {
  "log-driver": "json-file",
  "log-opts": {"max-size": "10m", "max-file": "3", "compress": "true"}
}' /etc/docker/daemon.json | sudo tee /etc/docker/daemon.json.new
sudo python3 -m json.tool /etc/docker/daemon.json.new >/dev/null
sudo mv /etc/docker/daemon.json.new /etc/docker/daemon.json
sudo systemctl restart docker

Step 3 — Add per-service overrides in Compose

Only for containers that exceeded 10m/day in Step 1 measurements.

COPY FILE — per-service override in docker-compose.yml (noisy API example).
  api:
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "5"
        compress: "true"

Step 4 — Deploy collector (Promtail or Fluent Bit)

Separate compose file in /opt/monitoring/ so logging stack updates don't touch app stack.

Step 5 — Verify end-to-end

RUN — paste into a shell on the host this section describes.
# Local rotation works
docker inspect --format='{{json .HostConfig.LogConfig}}' api | jq .

# Generate test log line
docker exec api sh -c 'echo "LOG_TEST_$(date -Iseconds)"'

# Find it in Loki (Grafana Explore) or:
curl -G 'http://loki:3100/loki/api/v1/query_range' \
  --data-urlencode 'query={container=~".*api.*"} |= "LOG_TEST"'

Sample output — local LogConfig and Loki end-to-end test (excerpt)

{"Type":"json-file","Config":{"compress":"true","max-file":"5","max-size":"50m"}}
{"status":"success","data":{"resultType":"streams","result":[{"stream":{"container":"api"},"values":[["1788107118000000000","LOG_TEST_2026-08-30T16:25:18+00:00"]]}]}}

Meaning: the running API has the intended local rotation and Loki returned the unique test line with nanosecond timestamp and container label. Empty result means collector discovery, labels, query range, or sink delivery failed; empty LogConfig means the container was not recreated with the intended file.

Step 6 — Alert on disk (don't skip)

Monitor /var/lib/docker and root filesystem at 80%. Log rotation prevents surprises but images/volumes also grow.

Part 7 — Formulas and stack-level math

What this part is for: cover: Formulas and stack-level math.
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.

Per-container disk cap

max disk per container = max-size × max-file
json-file: max-size 10m, max-file 3 → 10 × 3 = 30 MB
local: max-size 20m, max-file 5 → 20 × 5 = 100 MB

Whole stack total (worst case)

stack log disk ≈ Σ (max-size × max-file) for each container
12 containers, global default 10m × 3 = 30 MB each
12 × 30 MB = 360 MB stack ceiling
One override: api at 20m × 5 = 100 MB → total = 11×30 + 100 = 430 MB

Daily log growth estimate

daily_log_MB ≈ (lines_per_day × avg_bytes_per_line) ÷ 1,048,576
500,000 lines/day × 400 bytes/line = 200,000,000 bytes ≈ 191 MB/day
Need 7 days on disk with max-file 7 → max-size ≈ 191 MB per file → set max-size: 200m, max-file: 7
Or ship to CloudWatch and keep local at 50m × 3 for emergency debugging only

Headroom for /var/lib/docker

recommended: stack log cap ≤ 15% of free space reserved for docker (images + logs + volumes)
80 GB disk, 40 GB images/volumes, 10 GB free → budget logs ~1–1.5 GB max across all containers
430 MB planned stack logs + 30% margin = 560 MB → safe ✓

Part 8 — Complete production config examples

What this part is for: get a ready template for: Complete production config examples.
What you should do: COPY files; edit for your host; RUN apply/restart as labeled.

Full daemon.json (global safe defaults)

COPY FILE — create the config file named in the section (edit values first).
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "compress": "true"
  },
  "storage-driver": "overlay2"
}

Every new container: 30 MB log cap unless Compose overrides. Compressed rotations save disk.

Full docker-compose.yml (stack with mixed overrides)

COPY FILE — create the config file named in the section (edit values first).
services:
  nginx:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
        compress: "true"

  api:
    image: myapp/api:1.0.0
    restart: unless-stopped
    environment:
      APP_ENV: production
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"
        compress: "true"
        labels: "com.docker.compose.service,com.docker.compose.project"

  worker:
    image: myapp/worker:1.0.0
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "15m"
        max-file: "4"
        compress: "true"

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "5m"
        max-file: "2"
        compress: "true"

  # Sidecar example: ship api logs to Fluentd (optional)
  # api:
  #   logging:
  #     driver: fluentd
  #     options:
  #       fluentd-address: "127.0.0.1:24224"
  #       tag: "docker.api"
  #       fluentd-async: "true"

Part 9 — Inspect LogConfig and measure reality

What this part is for: get a ready template for: Inspect LogConfig and measure reality.
What you should do: COPY files; edit for your host; RUN apply/restart as labeled.

docker inspect LogConfig

See effective driver and options for a running container:

RUN — paste into a shell on the host this section describes.
docker inspect --format='{{json .HostConfig.LogConfig}}' api | jq .

Sample output — docker inspect ... LogConfig

{
  "Type": "json-file",
  "Config": {
    "max-file": "5",
    "max-size": "20m",
    "compress": "true"
  }
}

Meaning: Type is the effective driver and Config contains string-valued rotation options on this running container. Empty options mean inherited unlimited defaults or an old container; a different type changes valid options and on-disk format.

Log file path:

RUN — paste into a shell on the host this section describes.
docker inspect --format='{{.LogPath}}' api

Sample output — docker inspect --format '{{.LogPath}}' api

/var/lib/docker/containers/a1b2c3d4e5f6/a1b2c3d4e5f6-json.log

Meaning: this is the host path Docker currently writes for the container ID. Empty output is normal for some remote/none drivers; “No such object” means the name is wrong or the container was recreated.

Measure disk usage

RUN — paste into a shell on the host this section describes.
# Total Docker root (images + containers + logs + volumes)
sudo du -sh /var/lib/docker

# All container log directories
sudo du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -hr | head -20

# Find largest log files anywhere under docker
sudo find /var/lib/docker -name "*-json.log*" -exec du -h {} + | sort -hr | head -20

# Per-container log size (json-file)
CONTAINER=api
sudo du -h $(docker inspect --format='{{.LogPath}}' $CONTAINER)

Sample output — Docker and per-container disk usage (excerpt)

18G	/var/lib/docker
31M	/var/lib/docker/containers/a1.../a1...-json.log
9.8M	/var/lib/docker/containers/b2.../b2...-json.log
31M	/var/lib/docker/containers/a1.../a1...-json.log

Meaning: 18 GB includes images, layers, volumes, and logs; sorted log rows isolate the largest json-file consumers; the final row is the selected container. No matches means a different driver/path, not zero Docker usage. Permission errors require sudo.

Estimate daily growth (24-hour sample)

RUN — paste into a shell on the host this section describes.
# Record size now
SIZE1=$(sudo stat -c%s "$(docker inspect --format='{{.LogPath}}' api)")
echo "Start bytes: $SIZE1"

# Wait 24 hours (or measure over 1 hour and multiply)

# Record size later
SIZE2=$(sudo stat -c%s "$(docker inspect --format='{{.LogPath}}' api)")
echo "End bytes: $SIZE2"
echo "Growth MB: $(( (SIZE2 - SIZE1) / 1048576 ))"

Sample output — 24-hour log growth sample

Start bytes: 10485760
End bytes: 41943040
Growth MB: 30

Meaning: the active file grew by 30 MiB during the interval. Rotation between samples can make this subtraction negative or too small; for accurate daily volume also include rotated files or collector ingestion bytes.

Live log rate (rough)

RUN — quick log rate check on a running container.
# Lines per second over 10 seconds
timeout 10 docker logs -f api 2>&1 | wc -l

Sample output — 10-second docker logs -f line count

842

Meaning: 842 lines were observed in 10 seconds, roughly 84 lines/s during this sample. This includes retained lines emitted immediately unless --since is added, so treat it as a rough burst signal; zero means no output, wrong container, or a driver that does not support reads.

Part 10 — Log rotation behavior

What this part is for: cover: Log rotation behavior.
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.

When max-size is set and the active log file reaches that size:

  1. Docker closes the current file (e.g. ...-json.log)
  2. Renames it to ...-json.log.1 (and shifts .1 → .2, etc.)
  3. Opens a fresh ...-json.log
  4. If max-file is exceeded, the oldest numbered file is deleted
  5. If compress: true, rolled files become .gz
max-size 10m, max-file 3, compress true
Files on disk: -json.log (active, up to 10m), -json.log.1.gz, -json.log.2.gz
When active hits 10m again: .2.gz deleted, .1.gz → .2.gz, old active → .1.gz, new empty active file created

What docker logs shows: docker logs reads from the driver's stored buffer — for json-file/local, you see retained rotated content until max-file drops it. You cannot recover logs deleted after rotation.

Changing options: Editing daemon.json or Compose does not shrink existing files. Recreate the container (docker compose up -d --force-recreate api) or truncate manually (last resort — stop container first).

Part 11 — OOM and disk-full scenarios

What this part is for: cover: OOM and disk-full scenarios.
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.

Disk full on /var/lib/docker

Memory pressure (less common for logging)

Logging drivers buffer minimally on json-file/local. Risk increases with fluentd without fluentd-async — blocked writes can slow apps. Remote driver outage + sync mode → apparent "hang" under load.

OOM killer takes your app container, not Docker's log writer — but full disk prevents journal writes and can cascade into service failures. Treat log disk as production-critical capacity.

Container won't start after log config change

Invalid option names or values in Compose cause create failure. Validate with docker compose config before deploy. Typo maxsize instead of max-size is ignored or rejected depending on Compose version.

Worked example — full calculation summary for our server

What this part is for: understand: Worked example — full calculation summary for our server.
What you should do: read this section unless a labeled block says otherwise.
Input: 80 GB disk · 12 containers · ~45 MB/day total stack log growth · 7 days debug retention desired on host

Global default (daemon.json): max-size 10m, max-file 3, compress true → 30 MB/container
Stack default total = 12 × 30 MB = 360 MB

api override (60% of logs ≈ 27 MB/day):
7 days × 27 MB ≈ 189 MB needed → max-size 20m × max-file 5 = 100 MB (compress ≈ 70 MB effective) + ship to central store for full retention

nginx (10 MB/day): 10m × 3 = 30 MB ✓ (default)
redis (0.5 MB/day): 5m × 2 = 10 MB ✓
workers × 9 (~8 MB/day combined): default 30 MB each → 270 MB; consider local driver at 10m×3 = 30 MB to save inode churn

Revised stack total: 100 + 30 + 10 + (9 × 30) = 410 MB worst case
With compress on rolled files: ~280–320 MB typical
vs 80 GB disk: < 0.5% — safe margin ✓

Daily growth check: 45 MB/day × 7 days = 315 MB — aligns with tuned caps

Master defaults lookup — quick reference

What this part is for: cover: Master defaults lookup — quick reference.
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
READ — diagram or folder layout. Do not paste into a terminal.
Option / Driver          │ Default (if omitted)     │ Our value      │ Why we changed
─────────────────────────┼──────────────────────────┼────────────────┼────────────────────────────
log-driver (daemon)      │ json-file                │ json-file      │ Tooling compatibility
json-file max-size       │ -1 (unlimited)           │ 10m global     │ Prevent disk fill
json-file max-file       │ 1                        │ 3 global       │ Keep 3 rotated files
json-file compress       │ false                    │ true           │ Save ~70% on rolled logs
local max-size           │ 20m                      │ 10m (workers)  │ Tighter cap on batch jobs
local max-file           │ 5                        │ 3 (workers)    │ Match retention needs
api max-size × max-file  │ unlimited × 1            │ 20m × 5        │ Noisy service
redis max-size × max-file│ unlimited × 1            │ 5m × 2         │ Minimal logging
Compose logging block    │ (inherits daemon)        │ per-service    │ Right-size each service
Log path (json-file)     │ .../ID-json.log          │ (automatic)    │ —
Precedence               │ daemon → container       │ Compose wins   │ Explicit overrides

Troubleshooting — symptom → cause → fix

What this part is for: cover: Troubleshooting — symptom → cause → fix.
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
READ — diagram or folder layout. Do not paste into a terminal.
Symptom                              │ Likely cause                         │ Fix
─────────────────────────────────────┼──────────────────────────────────────┼──────────────────────────────
/var/lib/docker at 100%              │ Unlimited json-file logs             │ Set max-size/max-file; truncate largest logs
daemon.json change had no effect     │ Existing containers not recreated    │ docker compose up -d --force-recreate
One container huge, others fine      │ Missing Compose logging override     │ Add logging.options to that service
docker logs empty but app prints     │ Wrong driver / buffer not flushed    │ Check LogConfig; app may log to file not stdout
Log settings show null in inspect    │ Built with wrong compose file        │ docker compose config; verify deployed file
413/502 unrelated — disk full        │ Root full, not app error             │ Free disk first, then restart containers
fluentd driver slow/stuck            │ Collector down, async off            │ fluentd-async: true; fix collector
compress not working                 │ Engine too old or typo               │ Docker 20.10+; key is "compress": "true"
Can't find log file                  │ local driver or different ID         │ docker inspect --format='{{.LogPath}}'
Rotation not happening               │ max-size unlimited (-1)              │ Set explicit max-size

Production checklist

What this part is for: cover: Production checklist.
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.
  1. Set global max-size and max-file in /etc/docker/daemon.json — never run unlimited json-file in production
  2. Enable compress: "true" for json-file on disk-constrained hosts
  3. Override noisy services in Compose (api, workers, proxies)
  4. Verify effective config: docker inspect LogConfig on each critical container after deploy
  5. Measure actual daily log growth after 24–48 hours; recalculate caps
  6. Alert on disk usage ≥ 80% on root and /var/lib/docker
  7. Document stack log budget: Σ(max-size × max-file) ≤ planned MB
  8. For compliance retention beyond a few days, ship logs to Loki, CloudWatch, ELK, or Graylog — not bigger local files alone (see Part 5)
  9. Ensure apps log to stdout/stderr, not only to files inside the container (files inside container aren't rotated by Docker)
  10. After daemon.json changes, schedule recreate window — restart affects all containers on the host
  11. Run docker system df regularly; logs are one slice — prune images/volumes too
  12. Never put secrets in env log options or debug printouts

Best practices

What this part is for: cover: Best practices.
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.

Wrapping up

What this part is for: cover: Wrapping up.
What you should do: read the explanation, then do only blocks labeled COPY / RUN / SQL.

Every Docker logging option has a default, a purpose, and a formula. The dangerous defaults are json-file with unlimited max-size and max-file: 1 without size — together they mean one file grows forever until the host dies.

Start with daemon.json caps (10m × 3 with compress is a solid baseline), override chatty services in Compose, measure real growth with du and docker inspect, and ship logs off-host when you need more than a few days of history. The method doesn't change — only your line counts and disk budget do.

Have a specific container eating disk or a stack size you're unsure about? Drop your container count and daily log MB in the comments.