Vấn đề log trong container
Docker mặc định dùng json-file log driver: mọi thứ app ghi ra stdout/stderr được append vào một file JSON trên host. Cơ chế này đơn giản nhưng có một điểm yếu: không có log rotation mặc định. Một container chạy console.log cho từng request, gặp retry loop log ra 200 dòng stack trace mỗi lần — vài ngày sau file log có thể lên đến hàng chục GB, nuốt sạch disk.
docker logs cũng không phải công cụ debug duy nhất. Container có thể không có shell (distroless, scratch), hoặc process chính đã exit — lúc đó bạn cần đến docker cp, docker export, hay thậm chí là debug container tạm. Bài này đi qua các log driver phổ biến, cách cấu hình rotation, và kỹ thuật debug container kể cả khi không có shell.
Tại sao logging trong container khác với logging truyền thống
Nguyên tắc vàng của Docker logging: container là cattle, không phải pet. Container được sinh ra, chạy vài giờ hoặc vài ngày, rồi bị replace bởi phiên bản mới. Nếu app của bạn ghi log vào file bên trong container, log sẽ biến mất ngay khi container bị xóa. Ổ cứng của container filesystem là ephemeral — trừ khi bạn mount volume, mọi thứ nằm trong writable layer đều bị mất khi container stop và remove.
Cách tiếp cận đúng: app log ra stdout và stderr, Docker capture hai stream này, sau đó log driver quyết định gửi log đi đâu — file trên host, syslog, Fluentd, Loki, CloudWatch, hay bất kỳ backend nào.
Đây là flow tổng quan:
flowchart LR
subgraph Container
APP["app ghi log"]
STDOUT["stdout"]
STDERR["stderr"]
end
APP --> STDOUT
APP --> STDERR
STDOUT --> DRIVER["log driver"]
STDERR --> DRIVER
DRIVER --> FILE["/var/lib/docker/.../json.log"]
DRIVER --> SYSLOG["syslog / journald"]
DRIVER --> FLUENTD["Fluentd / Fluent Bit"]
DRIVER --> LOKI["Loki / Grafana"]
DRIVER --> CLOUD["CloudWatch / Stackdriver"]
FILE --> ROTATION["log rotation"]
ROTATION --> DISK["host disk"]
App chỉ cần làm một việc duy nhất: viết log ra stdout và stderr. Mọi chuyện còn lại do Docker và infrastructure lo.
console.log(), print(), System.out.println(), logger.info() nên ghi ra stdout/stderr. Không dùng file appender như FileHandler, log4j FileAppender, hay winston.transports.File trừ khi bạn có lý do đặc biệt và đã mount volume cho thư mục log đó.Log driver — ai chở log đi đâu
Docker hỗ trợ nhiều log driver, mỗi driver quyết định nơi log từ stdout/stderr của container được gửi đến.
json-file — mặc định và cũng là nguyên nhân 40GB
Đây là log driver mặc định. Log được lưu thành file JSON dưới host:
/var/lib/docker/containers/<container-id>/<container-id>-json.log
Mỗi dòng là một JSON object chứa log, stream (stdout/stderr), và time. Định dạng này tiện cho docker logs parse, nhưng nếu không cấu hình rotation, file sẽ tăng vô hạn.
# Kiểm tra log driver hiện tại của một container
docker inspect --format='{{.HostConfig.LogConfig.Type}}' <container-name>
Mặc định trả về json-file.
Các log driver khác
| Driver | Dùng khi |
|---|---|
syslog | Gửi log qua Unix socket /dev/log, tích hợp với syslog daemon của host |
journald | Gửi vào systemd journal, phổ biến trên các distro dùng systemd |
fluentd | Gửi log đến Fluentd collector qua TCP/HTTP, phổ biến trong Kubernetes với EFK/EFKL stack |
awslogs | Gửi thẳng vào AWS CloudWatch Logs, không cần agent trung gian |
gcplogs | Gửi vào Google Cloud Logging (Stackdriver) |
loki | Gửi thẳng vào Grafana Loki qua HTTP push API |
splunk | Gửi vào Splunk HTTP Event Collector |
gelf | Gửi log qua UDP đến Graylog hoặc các server hỗ trợ GELF format |
none | Tắt hoàn toàn logging — không capture gì từ container |
Chọn driver dựa trên stack logging hiện có của team bạn. Ví dụ nếu team đã dùng Grafana + Loki để monitor, dùng loki driver là tự nhiên nhất. Nếu team chạy trên AWS và chưa có centralized logging, awslogs là lựa chọn ít setup nhất.
# Chạy container với log driver loki
docker run -d \
--log-driver=loki \
--log-opt loki-url="http://loki:3100/loki/api/v1/push" \
--log-opt loki-retries=5 \
--log-opt loki-batch-size=400 \
my-app:latest
json-file thì không. Nếu bạn cần guaranteed delivery, cân nhắc dùng sidecar container (pattern phổ biến trong Kubernetes) thay vì log driver.Log rotation — đừng để json-file ăn hết ổ cứng
Nếu bạn dùng json-file driver (hoặc bất kỳ driver nào ghi ra file local), rotation là bắt buộc.
Cấu hình qua --log-opt khi chạy container
docker run -d \
--log-opt max-size=10m \
--log-opt max-file=3 \
my-app:latest
max-size=10m: mỗi file log JSON tối đa 10MB. Khi đạt ngưỡng, Docker rotate file mới.max-file=3: giữ tối đa 3 file cũ (tổng cộng 4 file gồm file hiện tại). File cũ nhất bị xóa.
Một container Node.js trung bình ghi khoảng 5-10MB log mỗi ngày (tùy traffic). Với config trên, log chiếm tối đa ~40MB disk. So với 40GB trước đây của team mình — con số khác biệt 1000 lần.
Cấu hình global qua daemon config
Để áp dụng rotation cho tất cả container, cấu hình trong /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Sau đó restart Docker daemon:
sudo systemctl restart docker
Lưu ý: cấu hình global chỉ áp dụng cho container mới tạo. Container cũ vẫn giữ cấu hình cũ.
# Kiểm tra log config của container đang chạy
docker inspect --format='{{.HostConfig.LogConfig}}' <container-name>
--log-opt tag="{% raw %}{{.Name}}/{{.ID}}{% endraw %}" để thêm container name và ID vào mỗi log entry. Rất hữu ích khi log từ nhiều container đổ về cùng một destination.docker logs — công cụ đầu tiên khi cần debug
Dù bạn dùng log driver nào, docker logs luôn hoạt động với json-file, journald, và local driver (với các driver khác như fluentd hay awslogs, docker logs không trả về gì — log đã được forward đi nơi khác).
Các flag hữu ích
# Xem log realtime (follow)
docker logs -f my-container
# Chỉ xem 100 dòng cuối
docker logs --tail 100 my-container
# Xem log từ một thời điểm
docker logs --since 2024-06-18T10:00:00 my-container
docker logs --since 30m my-container
# Xem log đến một thời điểm
docker logs --until 2024-06-18T12:00:00 my-container
# Kết hợp
docker logs --tail 50 --since 1h -f my-container
# Hiển thị timestamp cho mỗi dòng
docker logs -t my-container
# Chỉ xem stderr (lọc bằng grep, vì docker logs không có flag riêng)
docker logs my-container 2>&1 | grep -i error
docker logs load toàn bộ log file vào memory nếu không dùng --tail. Với file log 40GB như câu chuyện đầu bài, câu lệnh này sẽ treo hoặc OOM kill. Luôn dùng --tail khi log file lớn.Log từ container đã chết
Container đã stop nhưng chưa bị remove vẫn giữ log:
# Xem log của container đã exit
docker logs exited-container
# Tìm container đã stop trong 24h qua
docker ps -a --filter "status=exited" --filter "since=24h"
docker exec — vào bên trong container
Khi log chưa đủ để chẩn đoán, bạn cần “vào trong” container để kiểm tra.
# Mở shell trong container
docker exec -it my-container sh
# hoặc bash nếu container có
docker exec -it my-container bash
docker exec tạo một process mới trong cùng namespace với container (PID, network, mount, IPC). Điều này có nghĩa:
- Bạn thấy cùng process list với container
- Bạn dùng chung network interface
- Bạn có thể truy cập filesystem của container
# Chạy một lệnh duy nhất không cần interactive
docker exec my-container cat /etc/nginx/nginx.conf
docker exec my-container ls -la /app/logs
docker exec my-container env | grep DATABASE
# Exec với user khác
docker exec -u root my-container whoami
# Exec với working directory cụ thể
docker exec -w /app/config my-container ls
Đừng exec vào production container để sửa code. Container là immutable infrastructure. Nếu bạn exec vào và apt-get install vim để sửa một file config, thay đổi đó mất ngay khi container restart hoặc redeploy. Đường dẫn đúng: sửa Dockerfile hoặc ConfigMap, rebuild, redeploy.
Ngoài ra, exec vào production container tạo rủi ro bảo mật: nếu process bạn chạy chiếm CPU/memory, nó ảnh hưởng trực tiếp đến app. Dùng docker exec để debug và quan sát, không phải để sửa.
Các lệnh debug nhanh không cần interactive shell
# Xem process đang chạy trong container
docker exec my-container ps aux
# Xem network connections
docker exec my-container netstat -tlnp
docker exec my-container ss -tlnp
# Xem disk usage trong container
docker exec my-container df -h
# Xem memory usage
docker exec my-container free -m
# Dump environment variables
docker exec my-container env | sort
docker cp — chuyển file giữa host và container
Đôi khi bạn cần lấy file từ container ra để phân tích, hoặc đưa file vào để debug.
# Copy từ container ra host
docker cp my-container:/app/config.json ./config.json
docker cp my-container:/var/log/app ./logs-backup/
# Copy từ host vào container
docker cp ./new-config.json my-container:/app/config.json
docker cp ./scripts/debug.sh my-container:/tmp/
# Copy cả thư mục (dùng -a để giữ permission)
docker cp my-container:/etc/nginx/. ./nginx-config-backup/
Các use case thực tế của docker cp
# Lấy core dump để phân tích crash
docker cp my-container:/tmp/core.12345 ./
# Lấy database dump đã export trong container
docker exec my-container pg_dump -U postgres mydb > dump.sql
# hoặc
docker cp my-container:/tmp/dump.sql ./
# Đưa script debug vào container distroless (mount tạm rồi exec)
# Sẽ nói kỹ hơn ở phần dưới
# Lấy file log khi docker logs không đủ (app ghi log ra file)
docker cp my-container:/app/logs/error.log ./error-$(date +%Y%m%d).log
Debug distroless container — khi không có shell
Distroless images (như gcr.io/distroless/static-debian12, gcr.io/distroless/java17) là xu hướng hiện đại: chỉ chứa app và runtime dependencies, không có package manager, không shell, không coreutils. Nhỏ, an toàn, ít CVE — nhưng debug thì khó hơn.
Một distroless container thậm chí không có sh. docker exec -it distroless-container sh sẽ báo lỗi:
OCI runtime exec failed: exec failed: unable to start container process:
exec: "sh": executable file not found in $PATH
Chiến lược 1: docker cp + docker inspect
Dùng docker cp để lấy file ra ngoài phân tích, docker inspect để xem metadata:
# Xem toàn bộ config của container
docker inspect distroless-container | jq '.[0].Config'
# Xem mounted volumes
docker inspect --format='{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' distroless-container
# Lấy binary ra kiểm tra
docker cp distroless-container:/app/my-binary ./my-binary
file ./my-binary
ldd ./my-binary
Chiến lược 2: Ephemeral debug container (Linux kernel 5.1+)
Nếu kernel host hỗ trợ, bạn có thể tạo một debug container chia sẻ PID namespace với distroless container:
# Chạy busybox container dùng chung PID namespace của container cần debug
docker run -it --rm \
--pid=container:distroless-container \
--net=container:distroless-container \
--cap-add=SYS_PTRACE \
busybox sh
Bây giờ bạn đang ở trong busybox shell nhưng thấy process của distroless container qua /proc. Bạn có thể:
ps aux # thấy process của distroless container
cat /proc/1/cmdline # xem command của PID 1
cat /proc/1/environ | tr '\0' '\n' # xem environment variables
ls -la /proc/1/fd # xem file descriptors đang mở
cat /proc/1/status # xem memory, thread info
strace -p 1 # trace system calls của PID 1
Chiến lược 3: Mount volume có busybox binary
Trước khi chạy distroless container, mount một volume chứa static binary:
# Pull busybox static binary ra host (chỉ cần làm một lần)
docker create --name busybox-temp busybox
docker cp busybox-temp:/bin/busybox ./busybox
docker rm busybox-temp
# Chạy distroless container với busybox binary mounted
docker run -d --name my-distroless-app \
-v $(pwd)/busybox:/bin/busybox \
gcr.io/distroless/static-debian12:latest
# Bây giờ có thể exec busybox sh
docker exec -it my-distroless-app /bin/busybox sh
Cách này tiện cho dev/staging environment. Trong production, tốt hơn là build một debug variant của image bạn (thêm busybox hoặc alpine làm base layer thứ hai) hoặc dùng ephemeral container pattern.
Kubernetes: kubectl debug
Trong Kubernetes, kubectl debug (từ v1.18, GA từ v1.20) là giải pháp chính thức:
# Tạo ephemeral container trong cùng pod
kubectl debug -it my-pod --image=busybox --target=my-container
Ephemeral container dùng chung process namespace và có thể attach strace, tcpdump, hoặc bất kỳ tool debug nào.
Tại sao distroless không có shell? Distroless images chỉ chứa app của bạn và runtime libraries tối thiểu — không package manager (apt, apk), không shell (sh, bash), không coreutils (ls, cat, curl). Lợi ích:
- Image nhỏ hơn 50-80% so với phiên bản có full OS
- Attack surface giảm đáng kể: không có shell nghĩa là attacker không thể
execvào và chạy lệnh tùy ý nếu exploit được app - Ít CVE hơn vì không có OS packages
- CI/CD nhanh hơn vì image nhỏ
Đánh đổi là debug khó hơn — nhưng với các kỹ thuật trên, bạn vẫn có đủ công cụ để xử lý.
docker top và docker stats — giám sát không cần exec
Không phải lúc nào cũng cần exec vào container để xem thông tin. Docker cung cấp các lệnh lấy thông tin từ bên ngoài qua Docker API.
docker top — xem process list
# Xem process đang chạy trong container (như ps aux)
docker top my-container
# Format output
docker top my-container -o pid,user,cmd,%cpu,%mem
Lệnh này tương đương ps aux bên trong container nhưng không cần shell, hoạt động với mọi container kể cả distroless.
docker stats — realtime CPU, memory, network, I/O
# Realtime stats cho tất cả container đang chạy
docker stats
# Stats cho container cụ thể
docker stats my-container
# Không stream liên tục, chỉ lấy một snapshot
docker stats --no-stream my-container
# Format output cho script
docker stats --no-stream --format \
"table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}" \
my-container
```text
Output điển hình:
```text
NAME CPU % MEM USAGE / LIMIT NET I/O BLOCK I/O
my-container 2.15% 156.3MiB / 3.844GiB 12.5MB / 8.3MB 45MB / 2.1MB
Đây là công cụ đầu tiên mình dùng khi nhận được alert “container dùng CPU cao” hoặc “memory tăng dần” — không cần SSH vào container, không cần cài đặt gì.
docker inspect — mọi metadata trong một lệnh
# Full JSON metadata
docker inspect my-container
# Lọc trường cụ thể
docker inspect --format='{{.State.Status}}' my-container
docker inspect --format='{{.State.StartedAt}}' my-container
docker inspect --format='{{.NetworkSettings.IPAddress}}' my-container
# Kiểm tra health status
docker inspect --format='{{.State.Health.Status}}' my-container
# Xem exit code của container đã stop
docker inspect --format='{{.State.ExitCode}}' my-container
# Restart count
docker inspect --format='{{.RestartCount}}' my-container
Mối liên hệ với PID 1 và signal handling
Logging không thể tách rời khỏi cách container lifecycle hoạt động. PID 1 trong container có trách nhiệm nhận và forward signal, nhưng cũng là process “sở hữu” stdout/stderr mà Docker capture.
Một sai lầm phổ biến: dùng shell script làm ENTRYPOINT chạy app dưới dạng child process. Khi đó, shell script (PID 1) chiếm stdout/stderr, còn log từ app (PID 7) có thể không được Docker log driver nhìn thấy — hoặc app không nhận được SIGTERM khi container stop vì shell script không forward signal.
Chi tiết về vấn đề này mình đã viết ở Container lifecycle & signal handling. Khi thiết kế logging strategy, hãy đảm bảo PID 1 đúng là app của bạn (dùng exec trong script, hoặc dùng CMD thay vì script làm entrypoint), để Docker capture đúng log stream và signal đến đúng process.
Chọn lựa thực tế cho từng quy mô
| Quy mô | Logging strategy | Ghi chú |
|---|---|---|
| Dev cá nhân | json-file + rotation | Đơn giản nhất, docker logs là đủ |
| Team nhỏ (1-3 server) | json-file + rotation + Docker Compose với volume cho log | Mount /var/log từ host để log không mất khi container restart |
| Team vừa (5-20 server) | fluentd hoặc loki driver + Grafana | Centralized log viewing, không cần SSH vào từng server |
| Enterprise / K8s | Fluent Bit daemonset → Loki/Elasticsearch hoặc Cloud-native (CloudWatch/Stackdriver) | Sidecar pattern nếu cần transform log trước khi gửi |
| AWS-only | awslogs driver → CloudWatch → CloudWatch Insights | Không cần maintain logging infrastructure riêng |
json-file + rotation là đủ. Khi team có alert rule cần search log từ nhiều server, đó là lúc chuyển lên centralized logging. Đừng over-engineer logging stack từ ngày đầu.Tổng kết
Logging và debugging container xoay quanh một nguyên tắc: app chỉ cần ghi ra stdout/stderr, mọi thứ khác do infrastructure lo. Cụ thể:
- Chọn log driver phù hợp với stack hiện có:
json-filecho dev,fluentd/lokicho production nhiều server,awslogs/gcplogscho cloud-native. - Luôn cấu hình log rotation nếu dùng
json-file—max-sizevàmax-filelà hai tham số cứu production khỏi disk full. docker logslà công cụ đầu tiên để debug — dùng--tail,--since,--until,-flinh hoạt.docker execđể chạy lệnh trong container, nhưng chỉ để quan sát, không sửa code.docker cpđể chuyển file giữa host và container khi cần phân tích dump, config, hoặc log file.- Distroless container vẫn debug được qua shared PID namespace, ephemeral container, hoặc mount busybox binary.
docker topvàdocker statscho phép giám sát process và resource từ bên ngoài, không cần exec.
Câu hỏi hay gặp
Q: Làm sao biết container đang dùng log driver nào?
docker inspect --format='{{.HostConfig.LogConfig.Type}}' <container-name>
Trả về json-file, fluentd, loki, v.v.
Q: docker logs có hoạt động với mọi log driver không?
Không. docker logs chỉ hoạt động với json-file, journald, và local driver. Với fluentd, awslogs, loki, gelf, log đã được forward đến destination, bạn cần xem log trực tiếp trên destination đó.
Q: Tôi có thể thay đổi log driver của container đang chạy không?
Không. Log driver được set khi container được tạo và không thể thay đổi. Muốn đổi driver, bạn phải stop container, tạo lại với --log-driver mới — hoặc cập nhật daemon config và tạo container mới.
Q: Làm sao debug container bị crash ngay khi start (exit code 1)?
# Xem log của lần chạy cuối cùng (container đã stop)
docker logs <container-name>
# Xem exit code và error message
docker inspect --format='{{.State.ExitCode}} - {{.State.Error}}' <container-name>
# Override entrypoint để vào shell thay vì chạy app
docker run -it --entrypoint sh my-image
# Nếu là distroless image, thử với busybox
docker run -it --entrypoint /bin/busybox -v $(pwd)/busybox:/bin/busybox my-image sh
Trường hợp cuối cho phép bạn kiểm tra filesystem, config, permission ngay cả khi app không chạy được.
Bài tiếp theo (Dữ liệu & Vận hành): Phần 8: Healthcheck & autoheal pattern, HEALTHCHECK instruction, health status, và autoheal sidecar.