Docker Compose Logging: Complete Setup & Best Practices

Docker Compose Logging: How to Configure, Rotate, and Centralize Container Logs

Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.

If you’ve ever run docker compose up on a production host and come back three weeks later to find your disk full, you already know why docker compose logging deserves more attention than it usually gets. By default, Docker happily writes every line your containers print to stdout/stderr into JSON files on disk — forever, unless you tell it otherwise.

This guide covers how docker compose logging actually works under the hood, how to configure logging drivers and rotation per-service, and how to ship logs somewhere useful instead of leaving them scattered across container filesystems.

Why Default Docker Compose Logging Is a Problem

Out of the box, Docker uses the json-file logging driver with no size limits and no rotation. Every console.log, every stack trace, every access log line gets appended to a file under /var/lib/docker/containers/<id>/<id>-json.log. On a busy API service or a chatty streaming transcoder, that file can grow into gigabytes within days.

The problems compound in a typical docker-compose.yml setup because:

  • There’s no rotation by default, so logs grow unbounded.
  • Every service inherits the same behavior unless explicitly overridden.
  • docker compose logs reads the entire backing file, which gets slower as it grows.
  • Multiple services logging heavily can fill root partitions on small VPS instances.
  • If you’re running Compose stacks on a budget VPS — say, a DigitalOcean droplet with a 25GB disk — an unrotated log file can take your whole box down. That’s the failure mode we’re trying to prevent here.

    How Docker Compose Logging Drivers Work

    Docker Compose doesn’t have its own logging system — it passes configuration straight through to the Docker Engine’s logging drivers. The driver determines where log output goes and how it’s formatted. Common options include:

  • json-file — the default; writes structured JSON to disk, supports rotation
  • local — a more efficient binary format, rotates by default, recommended over json-file for most cases
  • syslog — forwards to a syslog daemon (local or remote)
  • journald — forwards to systemd’s journal on Linux hosts
  • fluentd — ships logs to a Fluentd collector for centralized aggregation
  • none — disables logging entirely for that container
  • You set the driver per-service under the logging key in your docker-compose.yml. Here’s a minimal example using the local driver, which is a good default for most self-hosted setups:

    services:
      web:
        image: nginx:latest
        logging:
          driver: local
          options:
            max-size: "10m"
            max-file: "3"

    This caps each log file at 10MB and keeps a maximum of 3 rotated files, so the total footprint for that service tops out around 30MB regardless of how noisy the container gets.

    Configuring Log Rotation for Every Service

    Repeating the same logging block in every service is tedious and error-prone. Compose supports YAML anchors to define the config once and reuse it:

    x-logging: &default-logging
      driver: local
      options:
        max-size: "10m"
        max-file: "5"
    
    services:
      api:
        image: myapp/api:latest
        logging: *default-logging
    
      worker:
        image: myapp/worker:latest
        logging: *default-logging
    
      redis:
        image: redis:7-alpine
        logging: *default-logging

    This pattern keeps your docker-compose.yml DRY and guarantees no service slips through without rotation. If you’re managing multiple stacks, pair this with the strategies in our Docker Compose networking guide so both your networking and logging conventions stay consistent across projects.

    If you’d rather set rotation globally instead of per-project, edit /etc/docker/daemon.json on the host:

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

    Restart Docker afterward with sudo systemctl restart docker. Note that this only applies to newly created containers — existing ones keep their original logging config until recreated.

    Reading and Filtering Logs with the Compose CLI

    Once logging is configured, docker compose logs is your primary tool for day-to-day debugging. A few flags make it dramatically more useful than the bare command:

    # Follow logs in real time for all services
    docker compose logs -f
    
    # Follow logs for a single service only
    docker compose logs -f api
    
    # Show only the last 100 lines
    docker compose logs --tail=100 api
    
    # Include timestamps
    docker compose logs -f -t api
    
    # Show logs since a specific time
    docker compose logs --since="2026-07-09T00:00:00" api

    For quick filtering, pipe through grep:

    docker compose logs api | grep -i error

    This works fine for a single host, but it doesn’t scale once you have logs spread across multiple containers, multiple hosts, or a Swarm/Kubernetes migration down the line. That’s where centralized logging comes in.

    Centralizing Logs with Fluentd, Loki, or a Managed Service

    For anything beyond a single small VPS, shipping logs off-host is worth the setup time. Two common self-hosted approaches:

    Option 1: Fluentd driver

    services:
      app:
        image: myapp:latest
        logging:
          driver: fluentd
          options:
            fluentd-address: localhost:24224
            tag: myapp.{{.Name}}

    This requires a Fluentd container or daemon listening on the given address, which then routes logs to Elasticsearch, S3, or another backend.

    Option 2: Grafana Loki

    Loki pairs naturally with Compose stacks that already expose Prometheus metrics. Install the Loki Docker driver plugin, then configure services to use it:

    services:
      app:
        image: myapp:latest
        logging:
          driver: loki
          options:
            loki-url: "http://localhost:3100/loki/api/v1/push"
            loki-retries: "3"
            loki-batch-size: "400"

    If self-hosting a full logging stack feels like overkill for your team size, a managed log management service removes the operational burden entirely. BetterStack offers log collection, alerting, and uptime monitoring in one dashboard, with a straightforward Docker/Compose integration — worth considering if you’d rather not run and patch your own Loki or Elasticsearch cluster. It plugs in well alongside the container health checks discussed in our guide to monitoring Docker containers.

    Debugging Common Docker Compose Logging Issues

    A few issues come up repeatedly when teams first configure logging:

  • “No such file or directory” on log driver plugins — install the driver plugin first with docker plugin install grafana/loki-docker-driver:latest --alias loki, then restart Docker.
  • Logs missing after switching drivers — drivers like syslog, fluentd, and loki don’t support docker compose logs for historical output; you must query the downstream system instead.
  • Container won’t start after adding logging config — validate your YAML indentation; a misplaced options key under logging is a common typo.
  • Rotation isn’t happening — confirm you’re using local or json-file with max-size set; other drivers handle retention differently or not at all.
  • Disk still filling up — check for orphaned volumes or bind-mounted log directories that bypass the Docker logging driver entirely, common with apps that write directly to /var/log inside the container.
  • For reference, the official Docker logging documentation lists every driver and its supported options in detail — worth bookmarking when you hit a driver-specific edge case.

    Choosing a Logging Strategy Based on Scale

  • Single small VPS, low traffic: local driver with max-size: 10m and max-file: 3 is sufficient.
  • Single busy server, multiple services: Use the YAML anchor pattern above, and consider journald if you already use journalctl for other host services.
  • Multi-host or team environment: Ship to Loki, Elasticsearch, or a managed provider like BetterStack so logs survive host failures and are searchable across services.
  • Compliance-sensitive workloads: Ensure retention policy and access controls exist at the aggregation layer, not just on individual containers.
  • Picking the right tier upfront saves you from a painful migration later — moving from unrotated json-file logs to a centralized system after a disk-full incident is a bad way to learn this lesson.

    Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.

    FAQ

    Q: Does docker compose logging affect performance?
    A: Minimally for local or json-file drivers under normal load. High-throughput logging (thousands of lines per second) can add measurable overhead, especially with network-based drivers like syslog or fluentd if the remote endpoint is slow to acknowledge writes.

    Q: Can I set different logging configs for different services in the same file?
    A: Yes. The logging key is defined per-service, so you can mix drivers — for example, local for a database and loki for a public-facing API that you want centrally searchable.

    Q: How do I clear existing log files without deleting containers?
    A: You generally can’t safely truncate the backing JSON file directly while a container is running. Instead, add rotation (max-size/max-file) and recreate the container with docker compose up -d --force-recreate, or use docker system prune cautiously for stopped containers.

    Q: Why doesn’t docker compose logs show anything after I switched to the syslog driver?
    A: docker compose logs reads from Docker’s internal log storage, which drivers like syslog, fluentd, and journald bypass. You need to query the destination system (syslog server, Fluentd backend, or journalctl) instead.

    Q: What’s the difference between json-file and local drivers?
    A: local uses a more compact binary format and enables rotation by default (with sane defaults even if you don’t set options), while json-file requires you to explicitly configure max-size and max-file or it will grow unbounded.

    Q: Should I log to stdout or write to a file inside the container?
    A: Log to stdout/stderr. Docker’s logging drivers are built around capturing standard streams; writing to files inside the container bypasses rotation, driver forwarding, and docker compose logs entirely, defeating the purpose of Compose’s logging config.

    Getting docker compose logging right isn’t complicated, but it’s easy to skip until it causes an outage. Set rotation limits from day one, pick a driver that matches your scale, and centralize logs before you actually need to search across five containers at 2 a.m.

    Comments

    Leave a Reply

    Your email address will not be published. Required fields are marked *