Docker Intermediate #6: Logging and Debugging
The final post of Docker Intermediate. Once you have several containers running, two questions hit first: where do logs come from, and where do you look when something doesn’t work. This post collects both.
This post in the Docker Intermediate series:
- #1 Multi-stage builds and image slimming
- #2 Build cache — layer ordering
- #3 docker compose basics — web + db
- #4 compose deep dive — depends_on, healthcheck, profiles
- #5 Environment variables and secrets
- #6 Logging and debugging ← this post
One-liner for container logging #
Docker’s recommended logging pattern is simple.
Apps log to stdout/stderr only. Docker takes the stream from there.
Don’t write to log files. Container filesystems are ephemeral (Basics #4), and aggregating logs from many containers requires the standard streams so tools can pick them up.
Once that habit is in place, docker logs, log drivers, and external aggregators like fluentd / Loki all flow through the same exit.
Quick notes per language #
| Language / framework | What to check |
|---|---|
| Python | print is buffered → set PYTHONUNBUFFERED=1. The logging module defaults to stderr. |
| Node.js | console.log/error go to stdout/stderr — fine |
| Go | log.Println → stderr — fine |
| Django | Make sure handlers in LOGGING are StreamHandler |
| Nginx | Default is /var/log/nginx/... — the standard pattern is to redirect with access_log /dev/stdout; error_log /dev/stderr; |
docker logs, again
#
The command from Basics #3, now from an operations angle:
docker logs -f myapp # follow
docker logs --tail 200 myapp # last 200 lines
docker logs --since 30m myapp # last 30 min
docker logs --since 2026-04-18T10:00 myapp # absolute time
docker logs --until 1h myapp # up to 1 hour ago
docker logs -t myapp # with timestampsCombining --since and --until is great when you want to slice around an incident:
docker logs --since 14:00 --until 14:10 myappWhere Docker stores stdout/stderr is the next step — log drivers.
Log driver — where the logs go #
Docker pipes container stdout/stderr through a log driver to wherever it’s configured. The default is json-file — JSON files on the host disk.
| Driver | Where |
|---|---|
json-file (default) | /var/lib/docker/containers/<id>/<id>-json.log |
local | An efficient json-file variant (compression and rotation by default) |
journald | systemd journal |
syslog | syslog daemon |
fluentd | fluentd daemon (external aggregator) |
gelf | Graylog |
awslogs | AWS CloudWatch Logs |
gcplogs | GCP Cloud Logging |
none | Discard logs (docker logs won’t work either) |
Per-container driver #
docker run -d --log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
myappservices:
web:
image: myapp
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"Those two options are essentially required in production:
max-size: 10m— rotate when one log file exceeds 10MBmax-file: 3— keep at most 3, drop the rest
Without them, log files grow forever and fill the disk. One of the most common Docker incidents.
Daemon-wide defaults #
If setting these per service is annoying, set them on the daemon.
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}Now every newly created container gets these defaults. Requires a daemon restart, so do it during a maintenance window in production.
docker compose logs — all together
#
Seeing logs from many services in one stream is one of Compose’s big conveniences.
docker compose logs # everything
docker compose logs -f # follow
docker compose logs --tail 100 # last 100 lines (per service)
docker compose logs web pg # specific services
docker compose logs --since 10m -f web # combined
docker compose logs --no-color # no color (for file redirection)
docker compose logs --no-log-prefix web # without "web | " prefixServices get colored prefixes so you can tell them apart at a glance.
web-1 | INFO 127.0.0.1 - - [20/May/2026 14:32:01] "GET / HTTP/1.1" 200
pg-1 | LOG: database system is ready to accept connections
redis-1| 1:M 20 May 2026 14:32:00.123 # Server initializedup --attach / --no-attach
#
docker compose up lets you choose which services’ logs to follow.
docker compose up --attach web --attach worker # only two services
docker compose up --no-attach pg --no-attach redis # exclude DB / cacheUseful when DB logs are too noisy or you want to focus on the service you’re debugging.
External log aggregation — one paragraph #
As the number of production containers grows, looking at host log files individually stops scaling. A one-paragraph survey:
- Loki + Promtail + Grafana — lightweight self-hosted; can run as a compose bundle
- Elastic Stack (ELK) — powerful but heavy: Elasticsearch + Logstash + Kibana
- Datadog / New Relic / Grafana Cloud — managed SaaS
- CloudWatch Logs / Cloud Logging — cloud-native environments
They all take container stdout, index it, and connect it to search / alerting. A topic for one step beyond this series, but worth knowing the names.
First debugging tool — docker exec
#
A command from Basics #3, now from a debugging angle:
docker compose exec web sh
# inside
ps aux # process tree
env | sort # env vars
cat /etc/resolv.conf # DNS settings
ls -la /app # working directorydocker compose exec lands you inside a running container. It doesn’t work for distroless / scratch bases — there’s no shell. (The trade-off from Intermediate #1.) For those, the pattern is to run a debug image alongside the target.
docker run -it --rm \
--network container:myapp-web-1 \
--pid container:myapp-web-1 \
nicolaka/netshoot--network container:X and --pid container:X share the target container’s network and process namespaces. Then commands inside nicolaka/netshoot (a network debug toolkit) like curl localhost:8000 reach the target’s port 8000. You loosen isolation and borrow the tools.
docker inspect — precision diagnosis
#
For peering into JSON state.
# status
docker inspect myapp-web-1 --format '{{.State.Status}}'
# health
docker inspect myapp-pg-1 --format '{{json .State.Health}}' | jq
# network
docker inspect myapp-web-1 --format '{{json .NetworkSettings.Networks}}' | jq
# mounts
docker inspect myapp-web-1 --format '{{json .Mounts}}' | jq
# start time
docker inspect myapp-web-1 --format '{{.State.StartedAt}}'
# Was it OOM-killed?
docker inspect myapp-web-1 --format '{{.State.OOMKilled}}'When you see OOMKilled: true, the container hit a memory limit — pair it with docker stats next.
docker stats — live resource usage
#
docker stats
# CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O
# a1b2c3d4... myapp-web-1 1.2% 120MiB / 7.7GiB 1.5% 12kB / 8kB
# e5f6g7h8... myapp-pg-1 0.1% 35MiB / 7.7GiB 0.4% 5kB / 4kBdocker stats streams live numbers for every container by default. For a single snapshot:
docker stats --no-streamIf you set resource limits, hitting 100% of MEM % will OOM-kill the container. In production, defining limits is standard.
services:
web:
image: myapp
mem_limit: 512m
cpus: 1.0
# or the deploy key (Swarm) — for plain compose, the form above worksDisk usage — docker system df
#
How much images / containers / volumes use:
docker system df
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 18 4 3.2GB 2.1GB (65%)
# Containers 6 1 12MB 5MB (41%)
# Local Volumes 8 3 420MB 280MB (66%)docker system df -vPair it with docker system prune from Basics #3 — your first command when disk fills up.
Looking inside an image — dive
#
When an image is suspiciously large, or you want to see which layer holds what — the external tool dive is excellent.
brew install dive
dive myapp:latestA TUI shows added / modified / deleted files per layer. Fastest way to spot what to slim. Sometimes used as a CI gate to prevent regression in image efficiency (build fails if a score drops below threshold).
docker compose top — processes inside containers
#
docker compose top
# myapp-web-1
# UID PID PPID CMD
# root 1234 1200 python app.py
# root 1456 1234 python app.py (worker)Same as ps aux after exec-ing in, but one command across all services. Quick way to spot zombie or unintended child processes.
docker events — Docker streaming what happened
#
Occasional use for incident tracking / automation.
docker events --filter container=myapp-web-1
# 2026-04-18T14:32:01 ... container die ...
# 2026-04-18T14:32:01 ... container start ...When a container keeps cycling up and down, this catches the pattern.
Common debugging flow #
| Symptom | First command |
|---|---|
| Container keeps exiting | docker logs <c>, docker inspect <c> --format '{{.State.OOMKilled}}' |
| Port not reachable from outside | docker port <c>, confirm the app binds to 0.0.0.0 inside |
| Containers can’t talk to each other | Same network? (inspect), service name typo |
| Disk full | docker system df, prune |
| Build is too big | dive, docker history |
| Compose isn’t merging as expected | docker compose config |
| Env var didn’t apply | docker compose run web env, the precedence table from #5 |
Wrapping the series #
The Intermediate toolkit, in one picture:
Build efficiency Run / operate
──────────────── ─────────────
#1 Multi-stage #3 compose — multiple containers
#2 BuildKit cache #4 healthcheck, depends_on, profiles
#2 mount cache #5 env vars and secrets
#2 External caches #6 logging and debuggingOn top of the single-container cycle from Basics, Intermediate added many containers + operational sense: one compose file, one command; healthcheck-driven start order; secrets out of images; logs together; debugging at the ready.
The next series is Docker Advanced. Deeper BuildKit features, multi-architecture builds, image security (non-root, distroless, Trivy scans, SBOM, cosign signing), resource limits and cgroups, and the operational details of production. One step up on top of the compose foundation built here.
Wrap-up #
The picture from this post:
- Apps log only to stdout/stderr; Docker handles the rest. No file logging.
docker logs --since/--until/--taillets you slice by time / volume.max-size+max-filelog options are essential in production — they prevent disk runaway.docker compose logsviews many services at once with colored prefixes.docker exec+inspect+statsare the first three debug tools. For distroless, the--network container:trick lets you bring a debug container alongside.divefor inside images,docker system df+prunefor disk,docker compose configfor verifying merged definitions.- Internalize the first command for each common symptom and tracing becomes fast.