أتمتة تنظيف Docker على السيرفر — سكربت أسبوعي يوفّر 40GB كل شهر
لو شغّلت df -h على سيرفر فيه Docker شغّال من 3 شهور، غالبًا هتلاقي 40% على الأقل من الـ disk ضايع في images قديمة و volumes يتيمة. السكربت اللي في المقال ده بيفضّي المساحة دي أسبوعيًا بدون ما تفقد أي container شغّال.
المشكلة باختصار
Docker بيحتفظ بكل image بنيتها، كل container وقف ومتشالش، وكل volume مش متربط بأي حاجة شغّالة. لو بتبني الـ image عشر مرات في اليوم (CI، تجارب محلية، hotfixes)، المساحة بتتراكم بسرعة رهيبة. على سيرفر CI عندي بيبني 20 مرة يوميًا، الـ disk بيمتلي في حوالي شهر من غير ما تلاحظ.
مثال بسيط جدًا للمبتدئين
تخيّل إنك كل ما تطبخ وجبة، بترمي الصحون في الحوض بدل ما تغسلها. أول يومين الموضوع عادي. بعد أسبوع، المطبخ كله مش قابل للاستخدام، والحوض بيفيض. Docker بالظبط كده — كل build بيسيب "صحون" (images قديمة، containers واقفة، caches، volumes يتيمة) لو متشالتش، بتبوّظ المطبخ. الفرق إن هنا "المطبخ" هو الـ disk بتاع السيرفر، ولمّا يمتلي بتوقع production.
الشرح العلمي
Docker معماريًا مبني على layered filesystem. كل docker build بينتج layers جديدة، والقديمة متبقاش مربوطة بأي tag أو container لكن مش بتتمسح تلقائيًا. نفس الحاجة بتحصل مع:
- Exited containers: كل
docker runبدون--rmبيسيب container جثّة لما يخلّص شغله. - Dangling images: images اتبنت وبعدين اتبنى فوقها tag جديد — الـ tag انتقل، لكن الـ image الأصلي لسه على الـ disk.
- Anonymous volumes: لو container كان بيستخدم volume من غير اسم، لمّا تشيل الـ container الـ volume بيفضل موجود للأبد.
- BuildKit cache: مجرد الـ build cache لوحده ممكن يوصل 30-50GB على سيرفر CI نشط.
ليه الحل اليدوي مش كفاية
الأمر المشهور docker system prune -a بيبان إنه يحل المشكلة. في الواقع بيعمل 3 مشاكل:
- بيمسح images هتحتاجها في البناء القادم — زي
node:20أوpostgres:16. نتيجة: أول build بعدها بياخد 3-5 دقايق زيادة عشان يسحب الـ base images من الـ registry تاني. - بيلمس volumes لو زوّدت
--volumes— ممكن تمسح volume فيه data إنتاج بالغلط لو مش مربوط حاليًا بأي container شغّال. - مبيتشرطش وقت — لو شغّلته الساعة 10 الصبح وحد بيعمل deploy في نفس اللحظة، بتخاطر بـ race condition على الـ images.
البديل: سكربت يتحكم بالظبط في إيه يتمسح وإيه يتساب، ويشتغل في وقت هادي (فجر الأحد مثلًا)، ويبعت لك تنبيه بالنتيجة.
السكربت الأسبوعي الكامل
الأدوات: docker + bash + cron + webhook Slack للتنبيه. احفظ الملف في /usr/local/bin/docker-cleanup.sh:
#!/usr/bin/env bash
set -euo pipefail
LOG="/var/log/docker-cleanup.log"
SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"
KEEP_DAYS=14
exec >> "$LOG" 2>&1
echo "=== cleanup started: $(date -Iseconds) ==="
before=$(df -BG / | awk 'NR==2 {print $4}' | tr -d 'G')
# 1. containers وقفت من أكثر من 24 ساعة
docker container prune -f --filter "until=24h"
# 2. images dangling + images غير مستخدمة أقدم من 14 يوم
docker image prune -af --filter "until=${KEEP_DAYS}*24h"
# 3. builder cache (BuildKit)
docker builder prune -f --filter "until=${KEEP_DAYS}*24h" --keep-storage 10GB
# 4. volumes يتيمة — بس اللي مفيهاش label keep=true
docker volume ls -qf dangling=true | while read -r vol; do
label=$(docker volume inspect -f '{{index .Labels "keep"}}' "$vol" 2>/dev/null || echo "")
if [[ "$label" != "true" ]]; then
docker volume rm "$vol" || true
fi
done
after=$(df -BG / | awk 'NR==2 {print $4}' | tr -d 'G')
freed=$((after - before))
msg="Docker cleanup done — freed ${freed}GB (was ${before}G, now ${after}G)"
echo "$msg"
if [[ -n "$SLACK_WEBHOOK" ]]; then
curl -sS -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"$msg\"}" "$SLACK_WEBHOOK" >/dev/null
fi
خلّيه قابل للتنفيذ:
sudo chmod +x /usr/local/bin/docker-cleanup.shجدولته في cron
# افتح crontab
sudo crontab -e
# أضف السطر ده (يوم الأحد 4 الفجر)
0 4 * * 0 SLACK_WEBHOOK=https://hooks.slack.com/services/XXX /usr/local/bin/docker-cleanup.sh
حماية الـ volumes المهمة
لو عندك volume فيه data إنتاج (postgres، redis persistence، uploaded files)، علّمه بـ label وقت الإنشاء:
docker volume create --label keep=true pg_prod_dataالسكربت بيتخطّاه تلقائيًا بسبب الشرط label == "true".
ملاحظة مهمة: Docker مش بيدعم إضافة label لـ volume موجود بدون إعادة إنشاء. لو عندك volumes قديمة مش معلّمة، الحل الآمن: اربط الـ volume بـ container شغّال دايمًا (مثلًا حطّه في docker-compose.yml مع restart: always). ساعتها مش هيظهر أصلًا في dangling=true.
الأرقام من سيرفر حقيقي
على سيرفر إنتاج بيعمل CI builds 15-20 مرة يوميًا (stack: Node.js + PostgreSQL + Redis):
- قبل السكربت: 92GB مستهلك من 100GB disk.
- أول تشغيل: حرّر 52GB (معظمها images قديمة من 6 شهور لـ Node.js 16 كانت بتترص من كل build قديم).
- من الأسبوع الثاني: بيحرّر 8-12GB كل أحد.
- على مدار شهر: متوسط 40GB محرّرة، مع ثبات الاستهلاك حوالين 55-65GB بدل ما يتجاوز 90GB.
trade-offs ومتى مش مناسب
المكسب: مش هتحتاج تتذكر تفضّي السيرفر يدويًا. بتكسب ~10-15 دقيقة شهريًا من شغل idle، والأهم إنك بتتفادى الكابوس بتاع disk full وسط deploy.
الثمن:
- أول build بعد الـ cleanup ممكن يطول دقيقة-دقيقتين لأن base images هتتسحب من الـ registry تاني.
- لو Slack webhook بتاعك معلّق أو URL غلط، مفيش تنبيه = ممكن تنسى الموضوع تمامًا. ضيف healthcheck خارجي (زي healthchecks.io) لو السكربت نفسه مهم عندك.
--keep-storage 10GBفي BuildKit ممكن يكون كبير أو صغير حسب حجم سيرفرك. ابدأ بـ 10 وعدّل لو شايف الـ cache بيمتلي أو الـ builds بطيئة.- السكربت مش بيتعامل مع images معلّمة بـ tag لكن مش مستخدمة حاليًا — هتفضل موجودة. ده مقصود عشان متكسرش rollback.
الافتراضات اللي الحل مبني عليها: Docker 23.0+ (BuildKit هو الـ default). لو عندك Docker أقدم، شيل سطر docker builder prune لأنه هيفشل. السكربت كمان بيفترض إن Docker data موجودة على الـ partition الرئيسية (/). لو عندك /var/lib/docker mounted على partition منفصل، غيّر الـ df -BG / لـ df -BG /var/lib/docker.
متى لا تستخدم هذه الطريقة
- سيرفر فيه image واحد بيشتغل شهور بدون builds — مفيش تراكم أصلًا، السكربت overkill.
- بيئة فيها compliance تتطلب audit log لكل حاجة اتمسحت — السكربت مبيحتفظش بـ manifest للـ images المحذوفة؛ هتحتاج تزوّد logging أعمق.
- Kubernetes cluster — في K8s، إدارة الـ image garbage collection شغل
kubeletنفسه عبرimageGCHighThresholdPercentوimageGCLowThresholdPercent. تشغيل السكربت ده على node فيه kubelet بيشتغل = مخاطرة إنك تحذف images الـ kubelet لسه محتاجها. - Docker Swarm نشط بـ services دايمًا scaling — استخدم فلاتر أكثر تحفظًا (
until=30*24h) لأن الـ services بتحتاج الـ images جاهزة أي لحظة.
الخطوة التالية
شغّل السكربت يدويًا دلوقتي مرة واحدة قبل ما تحطه في cron:
sudo bash /usr/local/bin/docker-cleanup.sh
tail -f /var/log/docker-cleanup.logلو توفّر أقل من 5GB في أول تشغيل، معناه السيرفر نظيف أصلًا والأتمتة مش أولوية — دوّر على سبب تاني لامتلاء الـ disk (logs، uploads، backups محلية). لو توفّر 20GB+، ضيفه للـ cron فورًا وروح نام.