Бэкап селфхоста с Raspberry Pi в стиле мейнфреймов (COBOL-стайл)
Я используют Raspberry Pi с родной ОС как домашний сервер для селфхоста. В моем случае там, помимо прочих контейнеров Docker, крутится Immich — отличная замена облачным галереям. Но когда база фотографий переваливает за пару десятков гигабайт, встает вопрос бэкапов.
Основная рабочая машина у меня на Fedora, и долгое время задача решалась простым перекидыванием файлов. Но у классического бэкапа через tar по SSH есть две проблемы:
- Это черный ящик. И если запускаешь копирование 25..50..100 ГБ вручную, то смотришь в висящий терминал. Непонятно, качается ли архив, с какой скоростью и не отвалилось ли соединение.
- Бэкап «на горячую». Копировать файлы базы данных PostgreSQL, пока работают контейнеры — лотерея. Рано или поздно такой бэкап окажется битым.
Чтобы решить это красиво и надежно, я написала bash-скрипт. Но вместо скучного вывода добавила в него эстетику старых мейнфреймов, перфокарт и языка COBOL. Никаких эмодзи — только хардкорный капс, строгий хронометраж (STEP), наборы данных (DATA SET) и коды завершения (CONDITION CODE).
Что под капотом?
Скрипт полностью автоматизирует процесс и работает по следующему алгоритму:
- Безопасная остановка: Скрипт по SSH мягко тушит Docker-контейнеры на малинке. Если остановка падает с ошибкой, он делает Rollback (запускает их обратно) и отменяет бэкап ABEND. Вывод докера перехватывается, чистится от ANSI-цветов и переводится в ВЕРХНИЙ РЕГИСТР для сохранения стиля.
- Предварительная оценка размера (Estimate): Перед запаковкой скрипт запрашивает у RPi точный размер папки в байтах через du -sb.
- Прогресс-бар и ETA: Полученный размер скармливается утилите pv (Pipe Viewer) на принимающей стороне (Fedora). Теперь в терминале работает полноценный прогресс-бар с расчетным временем до завершения джоба.
- Ротация: Скрипт оставляет только 4 последних архива (хранит историю за месяц), удаляя старые "наборы данных". Сохраняется всё ровно в ту же папку, откуда был вызван сам скрипт.
Тот самый код (BASH BATCH PROCESSING)
Запускается скрипт просто: ./immich_backup.sh. Пароль от sudo на малинке запрашиваться не должен (нужно настроить беспарольный sudo для пользователя, хотя он вроде и так из коробки настроен).
#!/bin/bash
# Настройки
REMOTE_ALIAS="rpi5"
NODE_NAME="${REMOTE_ALIAS^^}"
SOURCE_PARENT="/home/rachel"
SOURCE_NAME="immich"
TARGET_DIR="$(dirname "$(realpath "$0")")"
FILENAME="immich_week_$(date +%V).tar"
TARGET_PATH="$TARGET_DIR/$FILENAME"
log_msg() {
echo "$(date '+%Y/%m/%d %H:%M:%S') $1"
}
echo "************************************************************************"
echo "* PROGRAM-ID. IMMICH-BACKUP. *"
echo "* AUTHOR. SYSTEM ADMIN. *"
echo "* ENVIRONMENT. BASH BATCH PROCESSING. *"
echo "************************************************************************"
echo "IDENTIFICATION DIVISION."
echo "PROCEDURE DIVISION."
log_msg "INFO - RUN JOB IMMICH-BKP ON NODE $NODE_NAME"
log_msg "STEP 01 - ALLOCATE TARGET DATA SET $TARGET_DIR"
mkdir -p "$TARGET_DIR"
log_msg "STEP 02 - HALT REMOTE DOCKER CONTAINERS"
ssh $REMOTE_ALIAS "cd $SOURCE_PARENT/$SOURCE_NAME && sudo docker compose stop" 2>&1 | while IFS= read -r line; do
clean_line=$(echo "$line" | sed -r 's/\x1B\[[0-9;]*[a-zA-Z]//g')
log_msg "DOCKER: ${clean_line^^}"
done
if [ ${PIPESTATUS[0]} -ne 0 ]; then
log_msg "ABEND - RETURN CODE NOT 00 ON DOCKER STOP"
log_msg "STEP 02A - ATTEMPT ROLLBACK (RESTART CONTAINERS)"
ssh $REMOTE_ALIAS "cd $SOURCE_PARENT/$SOURCE_NAME && sudo docker compose start" >/dev/null 2>&1
log_msg "JOB TERMINATED DUE TO ABEND"
exit 1
fi
log_msg "STEP 02 - CONDITION CODE 0000"
log_msg "STEP 03 - ESTIMATE REMOTE DATA SET SIZE"
ESTIMATED_SIZE=$(ssh $REMOTE_ALIAS "sudo du -sb $SOURCE_PARENT/$SOURCE_NAME" | cut -f1)
if [[ -n "$ESTIMATED_SIZE" && "$ESTIMATED_SIZE" =~ ^[0-9]+$ ]]; then
log_msg "INFO - ESTIMATED SIZE: $ESTIMATED_SIZE BYTES"
log_msg "STEP 03 - CONDITION CODE 0000"
else
log_msg "ABEND - FAILED TO ESTIMATE DATA SET SIZE"
ssh $REMOTE_ALIAS "cd $SOURCE_PARENT/$SOURCE_NAME && sudo docker compose start" >/dev/null 2>&1
exit 1
fi
log_msg "STEP 04 - EXECUTE REMOTE ARCHIVE AND DATA TRANSFER"
ssh $REMOTE_ALIAS "sudo tar -cf - -C $SOURCE_PARENT $SOURCE_NAME" | pv -ptrbe -s "$ESTIMATED_SIZE" > "$TARGET_PATH"
if [ ${PIPESTATUS[0]} -eq 0 ]; then
log_msg "STEP 04 - CONDITION CODE 0000, DATA WRITTEN TO $TARGET_PATH"
else
log_msg "ABEND - DATA TRANSFER FAILED"
ssh $REMOTE_ALIAS "cd $SOURCE_PARENT/$SOURCE_NAME && sudo docker compose start" >/dev/null 2>&1
exit 1
fi
log_msg "STEP 05 - COMPUTE DATA SET ALLOCATION SIZE"
if [ -f "$TARGET_PATH" ]; then
FILE_SIZE=$(stat -c %s "$TARGET_PATH")
log_msg "INFO - DATA SET SIZE: $FILE_SIZE BYTES"
log_msg "STEP 05 - CONDITION CODE 0000"
else
log_msg "ABEND - DATA SET NOT FOUND"
exit 1
fi
log_msg "STEP 06 - RESTART REMOTE DOCKER CONTAINERS"
ssh $REMOTE_ALIAS "cd $SOURCE_PARENT/$SOURCE_NAME && sudo docker compose start" 2>&1 | while IFS= read -r line; do
clean_line=$(echo "$line" | sed -r 's/\x1B\[[0-9;]*[a-zA-Z]//g')
log_msg "DOCKER: ${clean_line^^}"
done
log_msg "STEP 06 - CONDITION CODE 0000"
log_msg "STEP 07 - EXECUTE ROTATION PROCEDURE (KEEP 4 GENERATIONS)"
cd "$TARGET_DIR" || exit
ls -t immich_week_*.tar 2>/dev/null | tail -n +5 | xargs -I {} bash -c "echo \"\$(date '+%Y/%m/%d %H:%M:%S') INFO - SCRATCH OLD DATA SET: {}\" | tr '[:lower:]' '[:upper:]' && rm -- \"{}\""
log_msg "STEP 07 - CONDITION CODE 0000"
echo "************************************************************************"
echo "* END OF JOB IMMICH-BKP. MAXIMUM CONDITION CODE 0000 *"
echo "************************************************************************"
В итоге мы имеем надежный, самодостаточный скрипт, за работой которого просто приятно наблюдать.
Полезные ресурсы и инструменты:
Официальный сайт проекта Immich
Документация по Docker Compose
Мануал по утилите pv (Pipe Viewer)
Официальный портал Raspberry Pi
---
/raspberry/