توی این درس یاد میگیری لاگ کانتینرها دقیقاً کجا و چطور ذخیره میشوند و چرا بدون تنظیم میتوانند دیسک سرور را پر کنند. با 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) یعنی «حداکثر ۳ دفتر نگه دار، هر کدام تا ۱۰ مگابایت؛ وقتی پر شد، قدیمیترین را دور بینداز». حافظهی وقایع محدود ولی همیشه تازه است.
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: لاگ کجاست؟ (json-file)
Section titled “مثال ۱: لاگ کجاست؟ (json-file)”یک daemon آزمایشی میسازیم و برای کوتاه شدن دستورها تابع D را تعریف میکنیم که دستور را داخل آن اجرا میکند:
docker run -d --privileged --name lx-lg-dind -e DOCKER_TLS_CERTDIR= docker:dind >/dev/nulluntil docker exec lx-lg-dind docker info >/dev/null 2>&1; do sleep 1; doneD() { docker exec lx-lg-dind "$@"; }D docker pull alpine >/dev/null 2>&1D docker run -d --name web alpine sh -c 'while true; do echo "درخواست جدید"; sleep 1; done' >/dev/nullsleep 3echo "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 webdriver پیشفرض: json-filedriver این کانتینر: 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 چه نشان میدهد؟
در کانتینر لاگ را روی stdout/stderr بنویس (یا فایل را به /dev/stdout لینک کن).
مثال ۲: بدون سقف، لاگ فقط رشد میکند
Section titled “مثال ۲: بدون سقف، لاگ فقط رشد میکند”D() { docker exec lx-lg-dind "$@"; }D docker run --name big alpine sh -c 'yes "0123456789-abcdefghij-log-line" | head -n 100000' >/dev/nullLP=$(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 “مثال ۳: چرخش لاگ برای یک کانتینر”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/nullLP=$(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”D() { docker exec lx-lg-dind "$@"; }D docker run --name mix alpine sh -c 'echo "پیام عادی"; echo "پیام خطا" >&2; echo "پیام عادی دوم"' >/dev/nullecho "--- همه:"; D docker logs mix 2>&1echo "--- فقط stdout (stderr دور ریخته):"; D docker logs mix 2>/dev/nullecho "--- با زمان (-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 را میبینی.
مثال ۵: logging driver local
Section titled “مثال ۵: logging driver local”local فرمت بهینهی داکر است: فشرده است و بهطور پیشفرض چرخش دارد (۵ فایل ۲۰ مگابایتی)، پس اگر تنظیم نکنی هم دیسک را پر نمیکند:
D() { docker exec lx-lg-dind "$@"; }D docker run --name loc --log-driver local alpine sh -c 'echo "از driver محلی"' >/dev/nullecho "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: localdocker logs: از driver محلیفایل: container.logdocker logs با local هم کار میکند. تفاوت: فایل خام container.log باینری و فشرده است (نه JSON قابلخواندن)، که ابزارهای ثالث که فایل JSON را میخوانند را از کار میاندازد. بسیاری ترجیح میدهند local را سراسری بگذارند.
مثال ۶: driver none و driverهای بیرونی
Section titled “مثال ۶: driver none و driverهای بیرونی”D() { docker exec lx-lg-dind "$@"; }D docker run --name nolog --log-driver none alpine echo "این هیچجا ذخیره نمیشود" >/dev/nullD docker logs nolog 2>&1 | head -2 | cut -c1-150Error 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 نیاز دارد و فقط روی کانتینرهایی که از این به بعد ساخته میشوند اثر میگذارد:
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" }}LXEOFdocker restart lx-lg-dind >/dev/nulluntil docker exec lx-lg-dind docker info >/dev/null 2>&1; do sleep 1; done; sleep 2echo "پیشفرض جدید: $(D docker info --format '{{.LoggingDriver}}')"D docker run --name after alpine echo hi >/dev/nullecho "کانتینر قدیمی (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 میکند.
مثال ۸: در Compose
Section titled “مثال ۸: در Compose”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"cd lgdocker 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 “مثال ۹: اورژانس، تهی کردن لاگ بزرگ”اگر دیسک پر شده و سقفی نبوده، فایل را بدون حذف کانتینر تهی کن:
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 فایل را باز نگه داشته و فضا آزاد نمیشود. بعد از اورژانس، سقف دائمی بگذار.
پشت پرده
Section titled “پشت پرده”برای هر کانتینر یک پروسهی کوچک به اسم 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برای کانتینر با drivernoneیا بعضی driverهای بیرونی ممکن نیست مگر dual logging فعال باشد.
جدولهای مرجع
Section titled “جدولهای مرجع”| 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 |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) بدون سقف ماندن
Section titled “۱) بدون سقف ماندن”مثال ۲. راهحل: 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 “۴) عدد بدون واحد یا اشتباه”docker run --rm --log-opt max-size=ده alpine true 2>&1 | head -1 | cut -c1-140docker: 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 پیشفرض لاگ داکر خودت چیست.
دیدن جواب
docker info --format 'driver پیشفرض: {{.LoggingDriver}}'driver پیشفرض: json-fileتمرین اصلی: چرخش لاگ را سراسری تنظیم کن. در یک daemon آزمایشی تازه، daemon.json را با max-size=100k و max-file=2 بنویس، restart کن و ثابت کن کانتینری که ۱۰۰ هزار خط مینویسد بیشتر از حدود ۲۰۰ کیلوبایت (دو فایل ۱۰۰ کیلوبایتی) لاگ ندارد.
دیدن جواب
docker rm -f lx-lg-e2 >/dev/null 2>&1docker run -d --privileged --name lx-lg-e2 -e DOCKER_TLS_CERTDIR= docker:dind >/dev/nulluntil docker exec lx-lg-e2 docker info >/dev/null 2>&1; do sleep 1; donedocker exec lx-lg-e2 docker pull alpine >/dev/null 2>&1docker 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/nulluntil docker exec lx-lg-e2 docker info >/dev/null 2>&1; do sleep 1; done; sleep 2docker exec lx-lg-e2 docker run --name noisy alpine sh -c 'yes "0123456789-abcdefghij-log-line" | head -n 100000' >/dev/nullLP=$(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 را بنویس. (روی داکر اصلی خودت اجرا کن؛ کانتینرها موقتیاند.)
دیدن جواب
docker run --name lx-e3a --log-driver none alpine echo hello >/dev/nulldocker logs lx-e3a >/dev/null 2>&1; echo "none → کد خروج docker logs: $?"docker run --name lx-e3b --log-driver local alpine echo hello >/dev/nullecho "local → docker logs: $(docker logs lx-e3b) (کد خروج $?)"docker rm lx-e3a lx-e3b >/dev/nullnone → کد خروج docker logs: 1local → docker logs: hello (کد خروج 0)آزمونک
Section titled “آزمونک”logging driver پیشفرض و سقف آن؟
پیشفرض json-file است و بدون max-size لاگ بینهایت رشد میکند.
دو گزینهی چرخش لاگ؟
مثلاً --log-opt max-size=10m --log-opt max-file=3.
تغییر log-opts در daemon.json…
کانتینرهای قدیمی را دوباره بساز.
docker logs چه چیزی را نشان میدهد؟
برنامه باید روی stdout/stderr بنویسد.
اگر دیسک پر شده و یک فایل لاگ عظیم است، اقدام فوری؟
rm فضا را آزاد نمیکند چون فایل باز است.
driver local چه مزیتی دارد؟
ولی فایل خام JSON نیست.
جمعبندی
Section titled “جمعبندی”- برنامه روی 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 IMG | driver فشرده با چرخش پیشفرض |
{"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 پیشفرض |