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

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.

Part 1 — How Docker logging works

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)

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

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.

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "compress": "true"
  }
}

After editing:

# 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 .
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.

services:
  api:
    image: myapp/api:latest
    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

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)

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

# 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)

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:

# /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
# 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:

services:
  api:
    image: myapp/api:latest
    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:

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

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

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

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.

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:

docker plugin install grafana/loki-docker-driver:latest \
  --alias loki \
  --grant-all-permissions

# Verify
docker plugin ls
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)

# 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
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:latest
    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

# 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
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)

# 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.confSystemMaxUse=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.

services:
  healthcheck-sidecar:
    image: busybox
    logging:
      driver: "none"

Part 6 — Practical walkthrough: first production host from zero

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)

# 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

Step 2 — Set daemon.json floor

sudo tee /etc/docker/daemon.json <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "compress": "true"
  }
}
EOF
sudo systemctl restart docker

Step 3 — Add per-service overrides in Compose

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

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

# 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"'

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

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

Full daemon.json (global safe defaults)

{
  "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)

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:latest
    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:latest
    restart: unless-stopped
    logging:
      driver: local
      options:
        max-size: "15m"
        max-file: "4"

  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

docker inspect LogConfig

See effective driver and options for a running container:

docker inspect --format='{{json .HostConfig.LogConfig}}' api | jq .

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

Log file path:

docker inspect --format='{{.LogPath}}' api

Measure disk usage

# 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)

Estimate daily growth (24-hour sample)

# 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 ))"

Live log rate (rough)

# Lines per second over 10 seconds
timeout 10 docker logs -f api 2>&1 | wc -l

Part 10 — Log rotation behavior

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

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

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

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

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

  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

Wrapping up

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.