Docker корисний, але з часом з'їдає гігабайти: старі образи, зупинені контейнери, кеши білда. Ручне прибирання працює рівно до того моменту, коли місце раптово закінчується у продакшені. Тож автоматизуємо регулярну очистку місця на диску у середовищі Docker на Linux за допомогою systemd timers і акуратного bash-скрипта. ⏰🧹

Покажу безпечну схему з «сухим запуском» (dry-run), можливістю виключати важливі ресурси за лейблами та логуванням у journal. Це проста й надійна автоматизація задач без залежності від зовнішніх інструментів, а також гарна альтернатива cron (про cron та systemd timers теж поговоримо).

How-to: автоматичне очищення через systemd timer

Крок 1. Перевірка доступу до Docker

Скрипт має виконуватися від root або користувача з правами доступу до Docker-сокета (член групи docker). Швидка перевірка:

docker info | head -n 5

Якщо помилка «permission denied», або запускайте від root (через sudo), або додайте користувача в групу docker і перелогіньтесь:

sudo usermod -aG docker "$USER" && newgrp docker

Крок 2. Створюємо bash-скрипт прибирання

Скрипт прибирає зупинені контейнери, старі (невикористані) образи, кеш білда і опційно — не використані томи. Є підтримка dry-run, агресивного режиму (-a для образів) і виключень за лейблом keep=true.

sudo tee /usr/local/bin/docker-autoclean.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

DAYS="${1:-14}"
HOURS=$((DAYS * 24))
DRY_RUN="${DRY_RUN:-true}"
AGGRESSIVE="${AGGRESSIVE:-false}"
INCLUDE_VOLUMES="${INCLUDE_VOLUMES:-false}"
KEEP_LABEL="${KEEP_LABEL:-keep=true}"

if ! command -v docker >/dev/null 2>&1; then
  echo "[ERR] docker не знайдено в PATH" >&2
  exit 1
fi

run() {
  if [ "$DRY_RUN" = "true" ]; then
    printf "[DRY RUN] %q " "$@"; printf "\n"
  else
    "$@"
  fi
}

echo "=== Docker auto-clean: days=$DAYS, hours=$HOURS, dry_run=$DRY_RUN, aggressive=$AGGRESSIVE, volumes=$INCLUDE_VOLUMES ==="

echo "-- До прибирання --"
(docker system df || true) 2>&1

# 1) Зупинені контейнери, старші за N днів (і без лейбла keep=true)
run docker container prune -f \
  --filter "until=${HOURS}h" \
  --filter "label!=$KEEP_LABEL"

# 2) Образи: за замовчуванням – тільки dangling; у AGGRESSIVE=true – всі невикористані
IMG_ARGS=()
[ "$AGGRESSIVE" = "true" ] && IMG_ARGS=("-a")
run docker image prune -f "${IMG_ARGS[@]}" \
  --filter "until=${HOURS}h" \
  --filter "label!=$KEEP_LABEL"

# 3) Кеш білда (BuildKit)
run docker builder prune -f --filter "until=${HOURS}h"

# 4) Необов'язково: невикористані томи без лейбла keep=true
if [ "$INCLUDE_VOLUMES" = "true" ]; then
  run docker volume prune -f --filter "label!=$KEEP_LABEL"
fi

# 5) Невикористані мережі
run docker network prune -f

echo "-- Після прибирання --"
(docker system df || true) 2>&1

exit 0
EOF

sudo chmod +x /usr/local/bin/docker-autoclean.sh

Порада: позначайте важливі ресурси лейблом keep=true, щоб скрипт їх не чіпав. Наприклад:

# Контейнер із захистом від прибирання
docker run -d --label keep=true --name critical nginx:stable

# Для образів (через Dockerfile)
# Dockerfile:
# LABEL keep=true

Крок 3. systemd service + timer

Створимо сервіс для одноразового запуску та таймер для планування.

# Сервіс
sudo tee /etc/systemd/system/docker-autoclean.service >/dev/null <<'EOF'
[Unit]
Description=Auto-clean old Docker images/containers
Wants=docker.service
After=docker.service

[Service]
Type=oneshot
Environment=DAYS=14
Environment=DRY_RUN=false
Environment=AGGRESSIVE=false
Environment=INCLUDE_VOLUMES=false
ExecStart=/usr/local/bin/docker-autoclean.sh "$DAYS"
# Щадні пріоритети I/O та CPU
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7

[Install]
WantedBy=multi-user.target
EOF

# Таймер (щодня з рандомною затримкою)
sudo tee /etc/systemd/system/docker-autoclean.timer >/dev/null <<'EOF'
[Unit]
Description=Daily Docker auto-clean timer

[Timer]
OnCalendar=daily
RandomizedDelaySec=30m
Persistent=true
Unit=docker-autoclean.service

[Install]
WantedBy=timers.target
EOF

# Активація
sudo systemctl daemon-reload
sudo systemctl enable --now docker-autoclean.timer

# Перевіримо стан таймера
systemctl list-timers docker-autoclean.timer

# Тестовий запуск сервісу (разово)
sudo systemctl start docker-autoclean.service

# Логи прибирання
journalctl -u docker-autoclean.service -n 100 --no-pager

За потреби змініть параметри середовища в .service (наприклад, DAYS=30, AGGRESSIVE=true). Також можна разово виконати «сухий» запуск:

sudo env DRY_RUN=true systemctl start docker-autoclean.service

Альтернативні способи

Через cron (якщо не використовуєте systemd)

Хоч я і раджу systemd timers за надійність і Persistency, cron теж працює:

# Приклад daily job (викличе скрипт з реальним прибиранням)
echo 'DRY_RUN=false /usr/local/bin/docker-autoclean.sh 14' | sudo tee /etc/cron.daily/docker-autoclean >/dev/null
sudo chmod +x /etc/cron.daily/docker-autoclean

Один рядок для разового прибирання

# Прибирає все невикористане старше 30 днів (обережно з -a)
docker container prune -f --filter until=720h
docker image prune -f -a --filter until=720h

GUI-спосіб (Portainer)

Якщо любите кліки, поставте Portainer та чистіть ресурси з веб-інтерфейсу:

docker volume create portainer_data
docker run -d -p 9443:9443 --name portainer \
  --restart=always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  portainer/portainer-ce:latest

Далі відкрийте https://SERVER:9443 → Home → Ваш Docker → Images/Containers/Volumes → Cleanup/Prune. У CE-версії розкладу немає, але для ручного керування це зручно.

FAQ

Чим відрізняється docker system prune від image/container prune?
system prune підчищає все одразу (контейнери, образи, мережі, кеш), але має менше тонких фільтрів. Окремі prune-команди дають більше контролю (лейбли, часові фільтри).

Чи безпечно ставити -a для образів?
Так, якщо впевнені, що образи не використовуються контейнерами. -a видаляє всі невикористані образи (не лише dangling). Для консервативного режиму залиште без -a.

Як виключити важливі ресурси?
Додавайте лейбл keep=true (контейнери: --label keep=true, образи: LABEL keep=true у Dockerfile). Скрипт фільтрує label!=keep=true.

Що таке Persistent=true у таймері?
Якщо сервер був вимкнений під час запланованого запуску, systemd виконає пропущене завдання при наступному старті.

Чи працює це з rootless Docker?
Так, якщо таймер і сервіс запущені від того ж користувача, що володіє rootless-сесією Docker (user-level systemd).

Чи торкнеться це Docker Compose?
Ні, працюючі контейнери не чіпаються. Образи, що використовуються контейнерами, також не видаляються (навіть у режимі -a).

Що робити, якщо місце вже закінчилось?
Швидке прибирання: зупинені контейнери + dangling-образи:

docker container prune -f
docker image prune -f

Потім увімкніть планове прибирання за інструкцією вище.

Як вимкнути/видалити таймер?
sudo systemctl disable --now docker-autoclean.timer, за потреби видаліть файли сервісу/таймера та зробіть daemon-reload.

Порада від Kernelka

Почніть із DRY_RUN=true, перегляньте лог (journalctl -u docker-autoclean.service), і лише потім вимикайте dry-run. Для прод-серверів раджу запуск у непіковий час і з RandomizedDelaySec, щоб хмара не «хуртовинила» I/O одночасно. Для особливо чутливих ворклоадів використовуйте лейбли keep=true скрізь, де потрібно гарантувати збереження.

Підсумок

  • Ми налаштували systemd timer і bash-скрипт для регулярного прибирання Docker-ресурсів.
  • Підтримані dry-run, агресивний режим і виключення за лейблами.
  • Це надійна автоматизація задач, краща за cron для більшості серверів.
  • Регулярна очистка зменшує ризики простоїв через заповнений диск.
  • За потреби доступний ручний і GUI-підхід (Portainer).