Когда в контейнере крутится прод, остановка ради бэкапа будет невозможной роскошью. Но docker volumes можно сохранять, без остановки, если правильно защититься от: открытых файлов, неконсистентных записей и гонок при чтении.
1️⃣ Проблема горячих бэкапов
Если контейнер пишет в volume во время архивации:
файлы могут быть разорваны,
часть транзакций попадет в архив, а часть нет,
каталог может измениться, пока его читает tar.
Чтобы избежать этого, нужен мягкий снапшот: заморозить структуру, дождаться закрытия файлов и архивировать.
Мы это реализуем через:
проверку открытых дескрипторов lsof
копию структуры через freeze-режим
tar с фиксированным списком файлов
2️⃣ Определяем volume и контейнер
VOLUME="mydata"
BACKUP_DIR="/backups"
STAMP=$(date +%Y%m%d-%H%M)
TARGET="$BACKUP_DIR/$VOLUME-$STAMP.tar.gz"
MOUNTPOINT=$(docker volume inspect "$VOLUME" -f '{{.Mountpoint}}')
3️⃣ Ждем, пока контейнер перестанет писать. Контейнер может продолжать работать, но нам важно дождаться момента тишины.
wait_until_quiet() {
local path="$1"
local delay=${2:-1}
while lsof +D "$path" >/dev/null 2>&1; do
echo "[*] Volume busy, waiting..."
sleep "$delay"
done
}
Это не блокирует процесс, просто ждёт микропаузу между транзакциями.
4️⃣ Создаем снапшот список файлов
Важно: tar должен использовать фиксированный список, а не динамическое дерево.
SNAPLIST=$(mktemp)
find "$MOUNTPOINT" -type f -o -type d | sed "s#^$MOUNTPOINT/##" > "$SNAPLIST"
Так мы замораживаем текущее состояние структуры.
5️⃣ Архивация без остановки контейнера
echo "[*] Waiting for files to become quiet..."
wait_until_quiet "$MOUNTPOINT"
echo "[*] Creating hot-backup: $TARGET"
tar -czf "$TARGET" \
-C "$MOUNTPOINT" \
-T "$SNAPLIST"
Файлы читаются быстрее, чем контейнер успевает их поменять, а вероятность гонки почти нулевая, потому что мы:
дождались отсутствия открытых дескрипторов
зафиксировали список файлов заранее
используем tar с указанием точных путей
6️⃣ Опционально: перезапрос тишины перед чтением больших файлов
Для больших БД/логов:
pause_if_lock() {
local file="$1"
while lsof "$file" >/dev/null 2>&1; do
echo "[*] File still in use: $file"
sleep 0.5
done
}
И внутри цикла архивации:
while read -r f; do
pause_if_lock "$MOUNTPOINT/$f"
done < "$SNAPLIST"
Это максимально приближает бэкап к снапшот-принципам.
7️⃣ Очистка
rm -f "$SNAPLIST"
echo "[+] Backup completed: $TARGET"
8️⃣ Пример полностью готового скрипта. Минимальный файл backup_volume.sh:
#!/usr/bin/env bash
set -euo pipefail
VOLUME="$1"
BACKUP_DIR="/backups"
STAMP=$(date +%Y%m%d-%H%M)
TARGET="$BACKUP_DIR/$VOLUME-$STAMP.tar.gz"
MOUNTPOINT=$(docker volume inspect "$VOLUME" -f '{{.Mountpoint}}')
wait_until_quiet() {
while lsof +D "$1" >/dev/null 2>&1; do
sleep 1
done
}
echo "[*] Volume mountpoint: $MOUNTPOINT"
SNAPLIST=$(mktemp)
find "$MOUNTPOINT" -mindepth 1 -printf '%P\n' > "$SNAPLIST"
echo "[*] Waiting for quiet state..."
wait_until_quiet "$MOUNTPOINT"
echo "[*] Archiving..."
tar -czf "$TARGET" -C "$MOUNTPOINT" -T "$SNAPLIST"
rm -f "$SNAPLIST"
echo "[+] Done: $TARGET"
BashTex 📱 #bash