رفتن به محتوا
LoopX

لاگ‌ها در داکر

توی این درس یاد می‌گیری لاگ کانتینرها دقیقاً کجا و چطور ذخیره می‌شوند و چرا بدون تنظیم می‌توانند دیسک سرور را پر کنند. با logging driver (json-file، local، none) آشنا می‌شوی، چرخش لاگ را با --log-opt max-size و max-file برای یک کانتینر تنظیم می‌کنی، همان را سراسری در daemon.json می‌گذاری، نتیجه را روی فایل‌های واقعی لاگ می‌بینی و با docker logs (و --since، --tail، -t) لاگ را می‌خوانی. تمرین اصلی: چرخش لاگ را سراسری تنظیم می‌کنی.

مسئله: لاگ بی‌صدا دیسک را می‌خورد

Section titled “مسئله: لاگ بی‌صدا دیسک را می‌خورد”

هر چیزی که برنامه‌ی داخل کانتینر روی stdout و stderr بنویسد، daemon داکر آن را (به‌صورت پیش‌فرض) در یک فایل JSON روی دیسک میزبان ذخیره می‌کند. این فایل به‌طور پیش‌فرض هیچ سقفی ندارد. اپی که پرحرف است یا در یک حلقه‌ی خطا گیر کرده، می‌تواند ظرف چند روز ده‌ها گیگابایت لاگ بنویسد و دیسک سرور را پر کند؛ و دیسک پر یعنی دیتابیس و همه‌ی چیزهای دیگر هم می‌شکنند.

تشبیه: دفتر ثبت با تعداد صفحه‌ی محدود

Section titled “تشبیه: دفتر ثبت با تعداد صفحه‌ی محدود”

لاگ مثل دفتر ثبت وقایع است. اگر هیچ‌وقت صفحه‌ای دور نریزی، دفتر کل اتاق را پر می‌کند. چرخش لاگ (log rotation) یعنی «حداکثر ۳ دفتر نگه دار، هر کدام تا ۱۰ مگابایت؛ وقتی پر شد، قدیمی‌ترین را دور بینداز». حافظه‌ی وقایع محدود ولی همیشه تازه است.

برنامه فقط روی stdout و stderr می‌نویسد؛ daemon با logging driver انتخاب‌شده آن را ذخیره می‌کند. با json-file روی دیسک و قابل‌خواندن با docker logs.

مثال ۱: لاگ کجاست؟ (json-file)

Section titled “مثال ۱: لاگ کجاست؟ (json-file)”

یک daemon آزمایشی می‌سازیم و برای کوتاه شدن دستورها تابع D را تعریف می‌کنیم که دستور را داخل آن اجرا می‌کند:

Terminal window
docker run -d --privileged --name lx-lg-dind -e DOCKER_TLS_CERTDIR= docker:dind >/dev/null
until docker exec lx-lg-dind docker info >/dev/null 2>&1; do sleep 1; done
D() { docker exec lx-lg-dind "$@"; }
D docker pull alpine >/dev/null 2>&1
D docker run -d --name web alpine sh -c 'while true; do echo "درخواست جدید"; sleep 1; done' >/dev/null
sleep 3
echo "driver پیش‌فرض: $(D docker info --format '{{.LoggingDriver}}')"
echo "driver این کانتینر: $(D docker inspect web --format '{{.HostConfig.LogConfig.Type}}')"
LP=$(D docker inspect web --format '{{.LogPath}}')
echo "فایل لاگ: ${LP%%/containers/*}/containers/<ID>/<ID>-json.log"
echo "--- نخستین خط خام فایل:"
D head -1 "$LP" | sed -E 's/"time":"[^"]*"/"time":"…"/'
echo "--- docker logs:"
D docker logs --tail 2 web
خروجی
driver پیش‌فرض: json-file
driver این کانتینر: json-file
فایل لاگ: /var/lib/docker/containers/<ID>/<ID>-json.log
--- نخستین خط خام فایل:
{"log":"درخواست جدید\n","stream":"stdout","time":"…"}
--- docker logs:
درخواست جدید
درخواست جدید

پیش‌فرض json-file است: هر خط خروجی یک شیء JSON با سه کلید log (متن)، stream (stdout یا stderr) و time. docker logs همین فایل را می‌خواند و به شکل متن ساده نشان می‌دهد.

⚡ بررسی سریع

برنامه‌ی تو لاگ را در یک فایل داخل کانتینر (/var/log/app.log) می‌نویسد. docker logs چه نشان می‌دهد؟

مثال ۲: بدون سقف، لاگ فقط رشد می‌کند

Section titled “مثال ۲: بدون سقف، لاگ فقط رشد می‌کند”
Terminal window
D() { docker exec lx-lg-dind "$@"; }
D docker run --name big alpine sh -c 'yes "0123456789-abcdefghij-log-line" | head -n 100000' >/dev/null
LP=$(D docker inspect big --format '{{.LogPath}}')
echo "اندازه‌ی لاگ بعد از ۱۰۰ هزار خط: $(( $(D stat -c %s "$LP") / 1000 )) KB"
echo "تعداد فایل‌های لاگ این کانتینر: $(D sh -c "ls $LP* | wc -l")"
خروجی
اندازه‌ی لاگ بعد از ۱۰۰ هزار خط: 10083 KB
تعداد فایل‌های لاگ این کانتینر: 1

۱۰۰ هزار خط کوتاه، حدود ۱۰ مگابایت لاگ ساخت (هر خط با پوشش JSON و زمان حدود ۱۰۰ بایت می‌شود) و فقط یک فایل است که هر چه اپ بنویسد بزرگ‌تر می‌شود. یک اپ پرحرف در چند روز به گیگابایت می‌رسد.

مثال ۳: چرخش لاگ برای یک کانتینر

Section titled “مثال ۳: چرخش لاگ برای یک کانتینر”
Terminal window
D() { docker exec lx-lg-dind "$@"; }
D docker run --name rotated --log-opt max-size=200k --log-opt max-file=3 alpine sh -c 'yes "0123456789-abcdefghij-log-line" | head -n 100000' >/dev/null
LP=$(D docker inspect rotated --format '{{.LogPath}}')
echo "گزینه‌ها: $(D docker inspect rotated --format '{{.HostConfig.LogConfig.Config}}')"
echo "فایل‌ها (نام ← اندازه به KB):"
D sh -c "for f in $LP*; do echo \" \${f##*/} ← \$(( \$(stat -c %s \$f) / 1000 )) KB\" | sed -E 's/[0-9a-f]{64}/<ID>/'; done"
echo "مجموع: $(D sh -c "cat $LP* | wc -c" | awk '{print int($1/1000)}') KB"
خروجی
گزینه‌ها: map[max-file:3 max-size:200k]
فایل‌ها (نام ← اندازه به KB):
<ID>-json.log ← 95 KB
<ID>-json.log.1 ← 200 KB
<ID>-json.log.2 ← 200 KB
مجموع: 496 KB

با max-size=200k هر فایل از ۲۰۰ کیلوبایت بزرگ‌تر نمی‌شود و با max-file=3 حداکثر ۳ فایل نگه داشته می‌شود (...-json.log فعلی و .1, .2 قدیمی‌تر). وقتی فایل جاری پر شود، به .1 تغییر نام می‌دهد و قدیمی‌ترین دور ریخته می‌شود. مجموع هرگز از حدود ۶۰۰ کیلوبایت رد نمی‌شود، حتی برای میلیون‌ها خط. (پیامدش: خطوط قدیمی‌تر از دست می‌روند؛ برای نگه‌داری بلندمدت لاگ را به سیستم مرکزی بفرست.)

مثال ۴: خواندن لاگ با docker logs

Section titled “مثال ۴: خواندن لاگ با docker logs”
Terminal window
D() { docker exec lx-lg-dind "$@"; }
D docker run --name mix alpine sh -c 'echo "پیام عادی"; echo "پیام خطا" >&2; echo "پیام عادی دوم"' >/dev/null
echo "--- همه:"; D docker logs mix 2>&1
echo "--- فقط stdout (stderr دور ریخته):"; D docker logs mix 2>/dev/null
echo "--- با زمان (-t):"; D docker logs -t --tail 1 mix 2>&1 | sed -E 's/^[0-9T:.-]+Z/<زمان>/'
echo "--- آخرین ۱ ثانیه (--since):"; D docker logs --since 1s web 2>&1 | wc -l | sed 's/^/ /'
خروجی
پیام خطا
--- همه:
پیام عادی
پیام عادی دوم
پیام خطا
--- فقط stdout (stderr دور ریخته):
پیام عادی
پیام عادی دوم
--- با زمان (-t):
<زمان> پیام خطا
--- آخرین ۱ ثانیه (--since):
1

توجه: stdout و stderr دو جریان جدا هستند، پس ترتیب نمایش آن‌ها در خروجی ترکیبی (2>&1) لزوماً با ترتیب نوشتن یکی نیست (در خروجی بالا «پیام خطا» اول آمد).

نکته‌های docker logs: --tail N آخرین N خط؛ -f دنبال‌کردن زنده؛ -t زمان؛ --since 10m یا --until؛ و چون stdout و stderr جدا ذخیره می‌شوند، با 2>/dev/null فقط stdout را می‌بینی.

local فرمت بهینه‌ی داکر است: فشرده است و به‌طور پیش‌فرض چرخش دارد (۵ فایل ۲۰ مگابایتی)، پس اگر تنظیم نکنی هم دیسک را پر نمی‌کند:

Terminal window
D() { docker exec lx-lg-dind "$@"; }
D docker run --name loc --log-driver local alpine sh -c 'echo "از driver محلی"' >/dev/null
echo "driver: $(D docker inspect loc --format '{{.HostConfig.LogConfig.Type}}')"
echo "docker logs: $(D docker logs loc)"
echo "فایل: $(D sh -c 'ls /var/lib/docker/containers/*/local-logs/' 2>/dev/null | head -2 | tr '\n' ' ')"
خروجی
driver: local
docker logs: از driver محلی
فایل: container.log

docker logs با local هم کار می‌کند. تفاوت: فایل خام container.log باینری و فشرده است (نه JSON قابل‌خواندن)، که ابزارهای ثالث که فایل JSON را می‌خوانند را از کار می‌اندازد. بسیاری ترجیح می‌دهند local را سراسری بگذارند.

مثال ۶: driver none و driverهای بیرونی

Section titled “مثال ۶: driver none و driverهای بیرونی”
Terminal window
D() { docker exec lx-lg-dind "$@"; }
D docker run --name nolog --log-driver none alpine echo "این هیچ‌جا ذخیره نمی‌شود" >/dev/null
D docker logs nolog 2>&1 | head -2 | cut -c1-150
خروجی
Error response from daemon: configured logging driver does not support reading

با --log-driver none لاگ جایی ذخیره نمی‌شود و docker logs خطای «configured logging driver does not support reading» می‌دهد. برای ارسال لاگ به سیستم مرکزی driver های دیگر هست (syslog، journald، fluentd، gelf، awslogs، …)؛ در داکر جدید برای driver های بیرونی هم یک کپی محلی نگه داشته می‌شود تا docker logs کار کند (dual logging). این driver ها نیاز به سرویس مقصد دارند و در این درس اجرا نشده‌اند.

مثال ۷: تنظیم سراسری در daemon.json

Section titled “مثال ۷: تنظیم سراسری در daemon.json”

برای اینکه هیچ کانتینری بی‌سقف نماند، پیش‌فرض را در daemon عوض کن. تغییر log-driver و log-opts به restart daemon نیاز دارد و فقط روی کانتینرهایی که از این به بعد ساخته می‌شوند اثر می‌گذارد:

Terminal window
D() { docker exec lx-lg-dind "$@"; }
docker exec -i lx-lg-dind sh -c 'mkdir -p /etc/docker && cat > /etc/docker/daemon.json' <<'LXEOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "1m",
"max-file": "2"
}
}
LXEOF
docker restart lx-lg-dind >/dev/null
until docker exec lx-lg-dind docker info >/dev/null 2>&1; do sleep 1; done; sleep 2
echo "پیش‌فرض جدید: $(D docker info --format '{{.LoggingDriver}}')"
D docker run --name after alpine echo hi >/dev/null
echo "کانتینر قدیمی (web) LogConfig: $(D docker inspect web --format '{{.HostConfig.LogConfig.Config}}')"
echo "کانتینر جدید (after) LogConfig: $(D docker inspect after --format '{{.HostConfig.LogConfig.Config}}')"
خروجی
پیش‌فرض جدید: json-file
کانتینر قدیمی (web) LogConfig: map[]
کانتینر جدید (after) LogConfig: map[max-file:2 max-size:1m]

کانتینر قدیمی تنظیم جدید را نگرفت (چون هنگام ساخت ثبت می‌شود) ولی کانتینر جدید max-size:1m و max-file:2 گرفت. پس بعد از تغییر سراسری، کانتینرهای قدیمی را دوباره بساز (docker compose up -d --force-recreate). اگر بخواهی برای یک کانتینر خاص جور دیگر بگذاری، --log-opt در docker run این پیش‌فرض را override می‌کند.

lg/compose.yaml
name: lxlg
services:
app:
image: alpine
command: sh -c 'while true; do echo working; sleep 1; done'
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Terminal window
cd lg
docker compose config | grep -A4 'logging:'
خروجی
logging:
driver: json-file
options:
max-file: "3"
max-size: 10m

بخش logging: هر سرویس همان --log-driver و --log-opt است (مقدارها داخل گیومه تا YAML عدد/متن را اشتباه نخواند). در عمل بهتر است پیش‌فرض سراسری در daemon.json بگذاری و در Compose فقط استثناها را بنویسی.

مثال ۹: اورژانس، تهی کردن لاگ بزرگ

Section titled “مثال ۹: اورژانس، تهی کردن لاگ بزرگ”

اگر دیسک پر شده و سقفی نبوده، فایل را بدون حذف کانتینر تهی کن:

Terminal window
D() { docker exec lx-lg-dind "$@"; }
LP=$(D docker inspect big --format '{{.LogPath}}')
echo "قبل: $(( $(D stat -c %s "$LP") / 1000 )) KB"
D truncate -s 0 "$LP"
echo "بعد: $(D stat -c %s "$LP") بایت"
خروجی
قبل: 10083 KB
بعد: 0 بایت

truncate -s 0 فایل محتوا را پاک می‌کند ولی فایل (و دسته‌ی باز شده‌ی daemon) را نگه می‌دارد، پس کانتینر ادامه می‌دهد. حذف فایل با rm کمک نمی‌کند، چون daemon فایل را باز نگه داشته و فضا آزاد نمی‌شود. بعد از اورژانس، سقف دائمی بگذار.

برای هر کانتینر یک پروسه‌ی کوچک به اسم containerd-shim نگه‌دار پروسه‌ی اصلی است. stdout و stderr برنامه به shim وصل می‌شود و shim هر خط را به logging driver می‌دهد. json-file هر خط را با time و stream به‌صورت یک خط JSON به انتهای فایل اضافه می‌کند؛ چرخش هم همین‌جا انجام می‌شود (تغییر نام و حذف). docker logs فایل را می‌خواند و JSON را به متن ساده برمی‌گرداند.

نکته‌های مهم:

  • لاگ بخشی از لایه‌ی کانتینر نیست و با docker rm پاک می‌شود. برای نگه‌داری بلندمدت، لاگ را به یک سیستم مرکزی بفرست.
  • اگر برنامه خط طولانی (چند ده کیلوبایت) بنویسد، به تکه‌های ۱۶ کیلوبایتی شکسته می‌شود.
  • max-size و max-file فقط برای json-file و local معنی دارند؛ هر driver بیرونی گزینه‌های خودش را دارد.
  • docker logs برای کانتینر با driver none یا بعضی driverهای بیرونی ممکن نیست مگر dual logging فعال باشد.
driver محل ذخیره docker logs چرخش
json-file (پیش‌فرض) JSON روی دیسک بله با max-size و max-file (پیش‌فرض: ندارد!)
local باینری فشرده روی دیسک بله پیش‌فرض (۵×۲۰ مگابایت)
none جایی نه —
syslog / journald سرویس سیستم بسته به نسخه مال سرویس مقصد
fluentd / gelf / awslogs … سرویس بیرونی بسته به نسخه مال سرویس مقصد
کار دستور
سقف برای یک کانتینر docker run --log-opt max-size=10m --log-opt max-file=3 …
driver برای یک کانتینر --log-driver local
سراسری daemon.json ← log-driver, log-opts + restart
فقط آخر docker logs --tail 100 -f NAME
بازه‌ی زمانی --since 30m --until 10m
مسیر فایل docker inspect --format '{{.LogPath}}' NAME

مثال ۲. راه‌حل: max-size/max-file سراسری یا local driver.

۲) انتظار اینکه تنظیم سراسری کانتینرهای قدیمی را عوض کند

Section titled “۲) انتظار اینکه تنظیم سراسری کانتینرهای قدیمی را عوض کند”

مثال ۷. راه‌حل: کانتینرها را دوباره بساز.

۳) لاگ در فایل داخل کانتینر

Section titled “۳) لاگ در فایل داخل کانتینر”

اپ در /var/log/app.log می‌نویسد و docker logs خالی است؛ ضمناً آن فایل بدون چرخش در لایه‌ی کانتینر رشد می‌کند. راه‌حل: روی stdout/stderr بنویس (یا لینک ln -sf /dev/stdout /var/log/app.log در Dockerfile، که nginx رسمی هم همین را می‌کند).

۴) عدد بدون واحد یا اشتباه

Section titled “۴) عدد بدون واحد یا اشتباه”
Terminal window
docker run --rm --log-opt max-size=ده alpine true 2>&1 | head -1 | cut -c1-140
خروجی
docker: Error response from daemon: failed to create task for container: failed to initialize logging driver: invalid size: 'ده'

مقدار max-size باید عدد با واحد k، m یا g باشد. راه‌حل: 10m.

۵) rm لاگ برای آزاد کردن فضا

Section titled “۵) rm لاگ برای آزاد کردن فضا”

فایل باز است و فضا آزاد نمی‌شود. راه‌حل: truncate -s 0.

✎ تمرینآسان

با docker info بگو driver پیش‌فرض لاگ داکر خودت چیست.

دیدن جواب
Terminal window
docker info --format 'driver پیش‌فرض: {{.LoggingDriver}}'
خروجی
driver پیش‌فرض: json-file
✎ تمرینمتوسط

تمرین اصلی: چرخش لاگ را سراسری تنظیم کن. در یک daemon آزمایشی تازه، daemon.json را با max-size=100k و max-file=2 بنویس، restart کن و ثابت کن کانتینری که ۱۰۰ هزار خط می‌نویسد بیشتر از حدود ۲۰۰ کیلوبایت (دو فایل ۱۰۰ کیلوبایتی) لاگ ندارد.

دیدن جواب
Terminal window
docker rm -f lx-lg-e2 >/dev/null 2>&1
docker run -d --privileged --name lx-lg-e2 -e DOCKER_TLS_CERTDIR= docker:dind >/dev/null
until docker exec lx-lg-e2 docker info >/dev/null 2>&1; do sleep 1; done
docker exec lx-lg-e2 docker pull alpine >/dev/null 2>&1
docker exec lx-lg-e2 sh -c 'mkdir -p /etc/docker; echo "{\"log-opts\":{\"max-size\":\"100k\",\"max-file\":\"2\"}}" > /etc/docker/daemon.json'
docker restart lx-lg-e2 >/dev/null
until docker exec lx-lg-e2 docker info >/dev/null 2>&1; do sleep 1; done; sleep 2
docker exec lx-lg-e2 docker run --name noisy alpine sh -c 'yes "0123456789-abcdefghij-log-line" | head -n 100000' >/dev/null
LP=$(docker exec lx-lg-e2 docker inspect noisy --format '{{.LogPath}}')
echo "گزینه‌ها: $(docker exec lx-lg-e2 docker inspect noisy --format '{{.HostConfig.LogConfig.Config}}')"
echo "مجموع لاگ: $(docker exec lx-lg-e2 sh -c "cat $LP* | wc -c" | awk '{print int($1/1000)}') KB (سقف حدود ۲۰۰)"
docker rm -f lx-lg-e2 >/dev/null
خروجی
گزینه‌ها: map[max-file:2 max-size:100k]
مجموع لاگ: 164 KB (سقف حدود ۲۰۰)
✎ تمرینسخت

نشان بده با --log-driver none لاگ ذخیره نمی‌شود ولی برنامه کار می‌کند، و با --log-driver local هم لاگ دیده می‌شود هم فایل فشرده است. برای هر کدام کد خروج docker logs را بنویس. (روی داکر اصلی خودت اجرا کن؛ کانتینرها موقتی‌اند.)

دیدن جواب
Terminal window
docker run --name lx-e3a --log-driver none alpine echo hello >/dev/null
docker logs lx-e3a >/dev/null 2>&1; echo "none → کد خروج docker logs: $?"
docker run --name lx-e3b --log-driver local alpine echo hello >/dev/null
echo "local → docker logs: $(docker logs lx-e3b) (کد خروج $?)"
docker rm lx-e3a lx-e3b >/dev/null
خروجی
none → کد خروج docker logs: 1
local → docker logs: hello (کد خروج 0)
؟ آزمونک
  1. logging driver پیش‌فرض و سقف آن؟

  2. دو گزینه‌ی چرخش لاگ؟

  3. تغییر log-opts در daemon.json…

  4. docker logs چه چیزی را نشان می‌دهد؟

  5. اگر دیسک پر شده و یک فایل لاگ عظیم است، اقدام فوری؟

  6. driver local چه مزیتی دارد؟

  • برنامه روی stdout/stderr می‌نویسد؛ daemon با logging driver ذخیره می‌کند. پیش‌فرض json-file بدون سقف است.
  • چرخش: --log-opt max-size=10m --log-opt max-file=3؛ سراسری در daemon.json (log-opts) با restart؛ فقط کانتینرهای جدید.
  • local فشرده و با چرخش پیش‌فرض؛ none بدون لاگ (و بدون docker logs).
  • docker logs --tail/-f/-t/--since؛ مسیر فایل با docker inspect --format '{{.LogPath}}'.
  • اورژانس: truncate -s 0؛ دائمی: سقف سراسری. برای نگه‌داری بلندمدت لاگ را به سیستم مرکزی بفرست.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
docker run --log-opt max-size=10m --log-opt max-file=3 IMGچرخش لاگ برای یک کانتینر
docker run --log-driver local IMGdriver فشرده با چرخش پیش‌فرض
{"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}}تنظیم سراسری در daemon.json (با restart)
docker logs --tail 100 -f -t NAMEآخرین خط‌ها، زنده، با زمان
docker logs --since 30m NAMEلاگ نیم‌ساعت اخیر
docker inspect NAME --format '{{.LogPath}}'مسیر فایل لاگ
truncate -s 0 $(docker inspect --format '{{.LogPath}}' NAME)تهی کردن لاگ در اورژانس
docker info --format '{{.LoggingDriver}}'driver پیش‌فرض