هذا المقال للمستوى المتوسط — موجّه لمهندسي DevOps وأصحاب سيرفرات الـ CI و الـ staging اللي بيشغّلوا Docker في الإنتاج.
لو فتحت df -h على سيرفر الـ CI ولقيت /var/lib/docker ماسك 178GB من أصل 200GB، Docker مش بيتخن. Docker بيخزّن كل image وكل build cache layer وكل container متوقّف وكل volume مهجور لحد ما تأمره يمسحهم بإيدك. السكربت اللي هتاخده هنا بيحرّر في المتوسط 40GB أسبوعيًا على سيرفر CI، شغّال على systemd timer، وما بيلمسش الـ images المرتبطة بـ containers حية.
أتمتة تنظيف Docker على السيرفر بسكربت bash و systemd timer
المشكلة باختصار
كل docker build بيضيف layers جديدة. كل deploy بيخلّف نسخة قديمة من الـ image. BuildKit بيحتفظ بـ cache يقدر يوصل لـ 80GB لو السيرفر بيـ build مرتين في اليوم. ومحدش بيلاحظ غير لما الـ pipeline يفشل بـ no space left on device الساعة 2 الصبح.
تخيّل المشكلة بمثال أبسط: مخبز بيخبز 50 رغيف كل يوم. اللي مش بيتباع بيتحط في الفريزر "تحسبًا". بعد 3 شهور الفريزر فيه 4000 رغيف بايت ومفيش مكان للجديد. Docker بيعمل نفس الكلام مع كل layer بيـ build، مع فرق إن الـ disk مش بيخبط في الكاشير — بيوقّف الـ deploy.
تقنيًا، Docker بيخزّن الـ images في /var/lib/docker/overlay2/ ككتل غير مضغوطة فوق بعض. كل layer ليه refcount داخلي. لمّا الـ image القديمة ما يبقاش معاها container شغّال ولا tag، بتفضل dangling — موجودة في القرص ومش مرجوعة لها، Docker مش هيمسحها لوحده.
ليه docker system prune لوحده مش الحل
الناس بتنصح بـ docker system prune -a وبس. المشكلة إن -a بيمسح أي image مش مرتبطة بـ container شغّال دلوقتي. لو عندك image لـ worker-v42 اتعمله rollback من ساعتين، هيتمسح. الـ deploy التالي اللي محتاج docker pull هيدفع 40-90 ثانية زيادة لو الـ registry في region بعيد.
الحل بيتقسم لـ 3 أوامر مع filters صريحة:
- امسح الـ containers المتوقّفة من أكتر من 24 ساعة — مفيش سبب تحتفظ بيها.
- امسح الـ images الـ dangling (اللي بدون tag أصلًا) — ضمّنها أمان 100%.
- امسح build cache أقدم من 7 أيام — الـ cache بعد أسبوع بقى عبء أكتر من فايدة.
السكربت كامل — قابل للنسخ
#!/usr/bin/env bash
set -euo pipefail
LOG_FILE="/var/log/docker-cleanup.log"
exec >> "$LOG_FILE" 2>&1
echo "=== $(date -Iseconds) docker cleanup start ==="
BEFORE=$(df --output=avail / | tail -1)
# 1) containers متوقّفة من أكتر من 24 ساعة
docker container prune --force --filter "until=24h"
# 2) images dangling (بدون tag)
docker image prune --force
# 3) build cache أقدم من 7 أيام
docker builder prune --force --filter "until=168h"
# 4) volumes مهجورة (مش مرتبطة بـ container)
docker volume prune --force
AFTER=$(df --output=avail / | tail -1)
FREED_KB=$((AFTER - BEFORE))
FREED_GB=$(awk "BEGIN {printf \"%.2f\", $FREED_KB/1024/1024}")
echo "freed: ${FREED_GB} GB"
echo "=== $(date -Iseconds) docker cleanup done ==="