لو السيرفر وقف الأسبوع اللي فات بسبب رسالة "no space left on device" وطلع السبب Docker images قديمة و build cache بـ 30GB، السكربت اللي هنا هيخلي ده ميحصلش تاني. نظام تنظيف أوتوماتيكي بـ systemd timer بيشتغل كل ليلة 3:30 صباحًا، بيشيل اللي مش مستخدم، ويبعتلك تنبيه Discord لو المساحة المسترجعة شاذة.
أتمتة تنظيف Docker بـ systemd timers
المشكلة باختصار
أي سيرفر بيشغّل Docker لشهور بدون صيانة بيتراكم فيه 4 أنواع من الحثالة: images قديمة مش مستخدمة، containers موقوفة متنساش، volumes يتيمة (dangling)، و build cache layers. المجموع بيوصل بسهولة لـ 30–80GB على سيرفر عنده 5 خدمات. ولما الـ disk يوصل 95% الـ Docker daemon نفسه ممكن يتوقف، ومعاه كل الـ containers. ده single point of failure مباشر.
مثال للمبتدئين: الحتة دي بالظبط زي إيه؟
تخيّل إنك بتطبخ كل يوم في نفس الكيتشن، وبعد كل وجبة بتسيب البواقي في الحلة وتبدأ وجبة جديدة بحلة تانية. بعد أسبوع الحلل كلها ممتلية ومفيش مكان للوجبة الجاية. Docker بالظبط كده: كل pull لـ image جديدة، كل build لـ Dockerfile، كل container بتوقفه، بيسيب بقايا. لو مش بتنضف، الكيتشن بيقفل.
التعريف الدقيق: Docker بيستخدم storage driver (غالبًا overlay2) بيخزّن كل layer من كل image كـ directory منفصل في /var/lib/docker/overlay2. لما image بتتبدّل بنسخة أحدث، الـ layers القديمة مش بتتمسح أوتوماتيكيًا؛ بتفضل موجودة على disk ومفيش pointer ليها — وده اللي اسمه "dangling".
ليه systemd timer مش cron؟
cron قديم، بيشتغل، لكن عنده 3 مشاكل على production:
- لو السيرفر كان مطفي وقت الـ schedule، الـ job مبيتنفذش أبدًا. systemd timer عنده
Persistent=trueبيعوّض التنفيذ أول ما السيرفر يقوم. - الـ logs بتروح لـ mail قد يكون مش مضبوط. systemd بيكتبها في journald مباشرةً وتقدر تشوفها بـ
journalctl -u docker-cleanup.service. - مفيش طريقة نظيفة تشغّل الـ job يدويًا بسرعة. systemd يكفي
systemctl start docker-cleanup.serviceبدون ما تعدل crontab.
مثال للمبتدئين على systemd
systemd هو اللي بيدير كل الخدمات على Linux الحديث. لما بتكتب systemctl status nginx ده systemd بيكلّمك. الـ timer بتاعه زي الـ alarm في موبايلك: بيصحّى خدمة معيّنة في وقت معيّن أو بعد فترة معيّنة من بدء السيرفر.
التعريف الدقيق: systemd timer هو unit file بامتداد .timer بيستهدف unit تاني بامتداد .service (غالبًا من نوع oneshot). الـ scheduler مدمج في PID 1، فمفيش daemon تاني لازم تشغّله.
السكربت الكامل — 3 ملفات
محتاجين ملفات 3: السكربت نفسه، service يشغّله، و timer يجدوله.
1) السكربت: /usr/local/bin/docker-cleanup.sh
#!/usr/bin/env bash
set -euo pipefail
LOG_TAG="docker-cleanup"
THRESHOLD_GB=50
before=$(df -BG /var/lib/docker | awk 'NR==2 {print $3}' | tr -d 'G')
# containers موقوفة أقدم من 24 ساعة
docker container prune -f --filter "until=24h"
# images مش مربوطة بـ tag (dangling) + unused أقدم من 72 ساعة
docker image prune -af --filter "until=72h"
# volumes يتيمة (مفيش container بيستخدمها)
docker volume prune -f
# build cache أقدم من 7 أيام
docker builder prune -af --filter "until=168h"
after=$(df -BG /var/lib/docker | awk 'NR==2 {print $3}' | tr -d 'G')
freed=$((before - after))
logger -t "$LOG_TAG" "freed ${freed}GB (before=${before}G after=${after}G)"
if [ "$freed" -gt "$THRESHOLD_GB" ]; then
curl -s -X POST "$DISCORD_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"content\":\"docker-cleanup حرّر ${freed}GB — قياس شاذ، راجع production.\"}"
fi
2) الـ service: /etc/systemd/system/docker-cleanup.service
[Unit]
Description=Docker resources cleanup
Wants=docker.service
After=docker.service
[Service]
Type=oneshot
EnvironmentFile=/etc/docker-cleanup.env
ExecStart=/usr/local/bin/docker-cleanup.sh
3) الـ timer: /etc/systemd/system/docker-cleanup.timer
[Unit]
Description=Run docker-cleanup daily at 03:30
[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.target
التفعيل
sudo chmod +x /usr/local/bin/docker-cleanup.sh
echo 'DISCORD_WEBHOOK=https://discord.com/api/webhooks/...' | sudo tee /etc/docker-cleanup.env
sudo chmod 600 /etc/docker-cleanup.env
sudo systemctl daemon-reload
sudo systemctl enable --now docker-cleanup.timer
systemctl list-timers | grep docker-cleanup
الـ trade-offs بوضوح
until=72hعلى images: بتكسب أمان أن image بتستخدمها للـ rollback الحالي متتمسحش. بتخسر ~8–15GB لو التحديثات كتيرة جدًا. لو بتـ deploy 10 مرات في اليوم قلّلها لـ24h.volume pruneبدون فلتر: بتكسب تنظيف كامل. بتخسر احتمال تمسح data في volume نسيت تربطه بـ container (حالة نادرة، لكن حصلت). لو عندك data مهم داخل named volumes، استخدمdocker volume prune --filter "label!=keep"وحطlabel: keepعلى الـ volumes الحساسة في الـ compose.- threshold 50GB للتنبيه: الرقم ده مبني على فرضية إن السيرفر الطبيعي عندك بيستهلك أقل من 50GB أسبوعيًا. لو السيرفر أكبر، ارفعها. القياسات الفعلية من 3 سيرفرات production عندي: متوسط التحرير اليومي 3–7GB، والأسبوعي 20–40GB.
- RandomizedDelaySec=15m: بتكسب عدم ضرب كل السيرفرات في نفس الثانية (مفيد لو عندك 20 سيرفر بيشيروا نفس الـ registry). بتخسر وقت تنفيذ غير ثابت بدقة.
متى لا تستخدم هذه الطريقة
لو بتشغّل CI/CD builder على نفس السيرفر، image prune -a هيمسح layers الـ base images اللي الـ builds بتعتمد عليها، وهيخلّي كل build بعدها يعيد الـ pull. في الحالة دي استبدل -a بـ --filter "label=auto-cleanup" وحط اللبل على images محددة بس. كمان لو عندك Docker Swarm بـ services قديمة متعلّقة بـ image قديم، مسحه هيكسر الـ redeploy عند restart. راجع docker image ls --filter "dangling=false" والـ services الحالية قبل ما تشغّل السكربت لأول مرة. وأخيرًا، لو سيرفر صغير أقل من 20GB disk كله، الـ pruning الأسبوعي مش هيكفي — محتاج monitoring لـ disk يوقف deploy لما يتعدى 80%.
الخطوة التالية
افتح السيرفر دلوقتي، شغّل df -h /var/lib/docker وشوف النسبة. لو فوق 60% نفّذ السكربت يدويًا مرة أولى بـ sudo /usr/local/bin/docker-cleanup.sh وشوف كم حرّر. بعدها ابنِ الـ timer وسيبه يشتغل. بعد 3 أيام افتح journalctl -u docker-cleanup.service --since "3 days ago" وتأكد إن الرقم اليومي مستقر. لو فيه يوم حرّر أكتر من 50GB هتوصلك رسالة Discord — ده مؤشر إن deploy أو build loop عمل مشكلة تستحق التحقيق.
المصادر
- Docker docs —
docker system prune: docs.docker.com/reference/cli/docker/system/prune - Docker docs — storage drivers (overlay2): docs.docker.com/engine/storage/drivers/overlayfs-driver
- systemd.timer manual: freedesktop.org/software/systemd/man/systemd.timer
- systemd.service manual (oneshot): freedesktop.org/software/systemd/man/systemd.service
- Docker reference —
docker builder prune: docs.docker.com/reference/cli/docker/builder/prune