Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors

Автоматическое резервное копирование по SFTP: bash-скрипт и конфиг

Linux и DevOps

Бэкап Linux-сервера по SFTP: bash-скрипт с дампом баз в Docker
Production-ready bash-скрипт для бэкапа Linux-сервера по SFTP: согласованные дампы баз в Docker, архивация томов и автоматизация через cron. Готовая схема.
бэкап, Linux, SFTP, Docker, bash, PostgreSQL, MySQL, cron, резервное копирование, DevOps, self-hosting

Содержание
  1. Бэкап Linux-сервера по SFTP: bash-скрипт с дампом баз в Docker
  2. Зачем нужен автоматический бэкап на отдельный сервер
  3. Почему файловая копия базы — это не бэкап базы
  4. Логический дамп против копирования файлов
  5. Структура решения
  6. Зависимости и подготовка окружения
  7. Конфигурационный файл backup.conf
  8. Разбор параметров
  9. Готовый скрипт backup.sh
  10. Как скрипт определяет тип базы
  11. Доступы читаются из метаданных, а не угадываются
  12. Redis
  13. Защита от битых дампов
  14. Опись окружения
  15. Что входит в SOURCE_DIRS и почему
  16. Права, chroot и проверка каталога на SFTP
  17. Про пароль и альтернативу — SSH-ключ
  18. Автоматизация через cron
  19. Проверка восстановления
  20. FAQ
  21. Можно ли использовать этот скрипт на сервере без баз данных?
  22. Нужно ли ставить клиенты баз (pg_dump, mysqldump) на хост?
  23. Почему дамп снимается по сети, а не через docker exec?
  24. Скрипт упал на одной базе — потеряю ли я весь бэкап?
  25. Дампит ли скрипт базы, установленные прямо в систему, а не в Docker?
  26. Почему дамп складывается в /opt, а не в отдельный каталог?
  27. Как добавить поддержку базы, которой нет в списке?
  28. Можно ли шифровать архив перед отправкой?
  29. Заключение

Бэкап Linux-сервера по SFTP: bash-скрипт с дампом баз в Docker

Надёжный бэкап сервера — это не только архив каталогов. Конфиги и файлы приложений сохранить просто, а вот с базами данных есть подвох: если заархивировать /var/lib/docker/volumes на работающем сервере, файлы PostgreSQL или MySQL попадут в копию «на горячую» — в несогласованном состоянии. При восстановлении такая база может не подняться вообще или подняться с повреждениями.

Ниже — production-ready bash-скрипт, который закрывает весь цикл: снимает согласованные логические дампы всех баз в Docker (PostgreSQL, MySQL/MariaDB, MongoDB, Redis), архивирует системные конфиги, данные приложений и тома Docker вместе с этими дампами и отправляет всё одним архивом на отдельный SFTP-сервер. Со всем сопутствующим: конфигом для переиспользования на разных серверах, логированием, обработкой ошибок, учётом chroot-ограничений и автоматизацией через cron.

Зачем нужен автоматический бэкап на отдельный сервер

Регулярное резервное копирование с отправкой на изолированный сервер защищает от целого набора сценариев потери данных:

  • отказ дисков или дискового контроллера;
  • ошибочное обновление или неудачная правка конфигурации;
  • случайное удаление или перезапись файлов;
  • повреждение данных внутри контейнеров и томов Docker;
  • компрометация сервера — если копии лежат на отдельном хосте, они переживут взлом основного.

Ключевое слово здесь — «отдельный». Бэкап, лежащий на том же диске, что и оригинал, не спасает от смерти диска; копия на том же сервере не переживёт его компрометацию.

Поэтому архив отправляется по SFTP на хранилище, изолированное от рабочего окружения, а сам процесс автоматизируется через cron, чтобы не зависеть от человеческой дисциплины.

Почему файловая копия базы — это не бэкап базы

База данных постоянно пишет на диск: страницы данных, журнал упреждающей записи (WAL у PostgreSQL, redo-лог у MySQL/InnoDB), временные файлы. В любой момент времени часть изменений уже на диске, часть ещё в памяти, часть в процессе записи.

Файлы на диске согласованы между собой только тогда, когда сервер базы остановлен штатно.

Когда tar проходит по каталогу с данными работающей базы, он копирует файлы по очереди, а база в это время продолжает их менять. В результате архив содержит «срез» из файлов, снятых в разные моменты, — состояние, которого на диске никогда не существовало целиком.

Для текстового конфига это не проблема, а для кластера базы данных — потенциально нерабочая копия.

Особенно коварно то, что такой архив выглядит нормальным: он создаётся без ошибок, занимает ожидаемый объём, распаковывается. Проблема всплывает только в момент восстановления, когда база отказывается стартовать — то есть в худший возможный момент.

Логический дамп против копирования файлов

Логический дамп — это выгрузка содержимого базы в виде набора команд (CREATE TABLE, INSERT и т.д.) или специального согласованного формата. Дамп снимается через клиент самой базы, которая гарантирует, что выгруженные данные соответствуют единому согласованному состоянию на момент начала операции.

У каждой СУБД для этого свой штатный инструмент:

  • PostgreSQLpg_dumpall (все базы и роли разом) или pg_dump (одна база);
  • MySQL / MariaDBmysqldump --all-databases;
  • MongoDBmongodump;
  • Redis — команда SAVE, сбрасывающая снимок памяти в файл dump.rdb.

Идея скрипта ниже простая: сначала снять дампы всех баз и положить их в каталог, который и так входит в список архивируемых, а потом запустить обычную архивацию файлов. Дампы согласованы и едут внутри того же общего архива.

Файлы баз тоже попадут в архив (как часть /var/lib/docker/volumes), но это уже подстраховка — разворачивать базы предполагается из дампов.

Структура решения

/usr/local/bin/backup.sh      # Основной скрипт: дамп баз + архивация + отправка
/etc/backup.conf              # Конфигурация (пути, доступы к SFTP)
/var/log/myproject-backup.log # Журнал выполнения

Логика разнесена по принципу «один скрипт — разные окружения»: код отдельно, конфигурация отдельно, единый лог. Дамп баз встроен в тот же скрипт и выполняется до архивации, так что отдельный файл под дампы не нужен.

Зависимости и подготовка окружения

Для работы скрипта нужны tar, sftp (пакет openssh-client), sshpass, базовые GNU-утилиты и docker — последний и так стоит на сервере, где крутятся контейнеры с базами.

Клиенты баз (pg_dumpall, mysqldump, mongodump, redis-cli) не нужно ставить на хост: скрипт поднимает одноразовый контейнер той же версии, что и сама база, и снимает дамп его клиентом по сети. То есть нужный инструмент всегда приходит вместе с образом базы.

Установка зависимостей на Debian/Ubuntu:

sudo apt update
sudo apt install -y openssh-client sshpass tar

На RHEL/CentOS/Rocky/AlmaLinux:

sudo dnf install -y openssh-clients sshpass tar

Размещение файлов и права:

sudo cp backup.sh /usr/local/bin/backup.sh
sudo chmod 700 /usr/local/bin/backup.sh
sudo chown root:root /usr/local/bin/backup.sh

sudo cp backup.conf /etc/backup.conf
sudo chmod 600 /etc/backup.conf
sudo chown root:root /etc/backup.conf

Право 600 на конфиг обязательно: в нём пароль от SFTP-сервера открытым текстом. При правах по умолчанию его смог бы прочитать любой пользователь системы.

Конфигурационный файл backup.conf

SOURCE_DIRS оформлен как bash-массив, а не строка: массив корректно переживает пробелы в путях и в целом надёжнее при разрастании списка каталогов.

############################################################
# REMOTE_BASE_DIR — каталог на удалённом SFTP-сервере.
#
# Указывается путь относительно корня SFTP-сессии,
# который видит пользователь (с учётом chroot).
#
# Как определить директорию:
#   sftp> pwd         — стартовая директория
#   sftp> mkdir test  — проверка прав на запись
#   sftp> rmdir test
#
# Примеры:
#   REMOTE_BASE_DIR="backup"
#   REMOTE_BASE_DIR="/home/backup"
#   REMOTE_BASE_DIR="data/backups"
############################################################
REMOTE_BASE_DIR="backup"

# Параметры подключения к SFTP-серверу
REMOTE_HOST="<IP>"
REMOTE_PORT="22"
REMOTE_USER="sftp_user"
REMOTE_PASSWORD="your_password"

# Каталоги для архивации (bash-массив).
# DB_DUMP_DIR (см. ниже) обязан попадать внутрь одного из этих путей.
SOURCE_DIRS=(
  "/etc"
  "/home"
  "/opt"
  "/var/lib/docker/volumes"
  # при необходимости добавьте свои каталоги данных, например:
  # "/srv/app-data"
)

# Куда складывать дампы баз перед архивацией.
# Должен находиться внутри одного из SOURCE_DIRS (здесь — внутри /opt).
DB_DUMP_DIR="/opt/backups/db"

# Локальный временный каталог и лог
LOCAL_TMP_DIR="/tmp"
LOG_FILE="/var/log/myproject-backup.log"

Разбор параметров

  • REMOTE_BASE_DIR — базовый каталог на SFTP-сервере; внутри него скрипт автоматически создаёт подкаталоги по имени хоста и дате.
  • REMOTE_HOST, REMOTE_PORT, REMOTE_USER, REMOTE_PASSWORD — параметры подключения. Пароль передаётся через sshpass.
  • SOURCE_DIRS — массив каталогов для архивации. Кроме привычных /etc, /home, /opt и томов Docker, сюда стоит добавить пути, где складывают данные сервисы, если они держат их вне перечисленного (нестандартные bind-монтирования).
  • DB_DUMP_DIR — каталог под дампы баз. Критично: он должен лежать внутри одного из SOURCE_DIRS, иначе дампы снимутся, но в архив не попадут. В примере он внутри /opt.
  • LOCAL_TMP_DIR — где собирается архив перед отправкой. На этом разделе должно хватить места под весь архив.
  • LOG_FILE — журнал операций.

Ротация архивов на стороне SFTP-сервера этим скриптом не делается — её обычно реализуют отдельной задачей на сервере хранения. Скрипт за собой удаляет только локальный временный архив после успешной отправки.

Готовый скрипт backup.sh

Скрипт читает конфигурацию, снимает дампы всех баз в Docker, архивирует каталоги вместе с дампами и передаёт архив на SFTP-сервер. Блок дампа баз написан устойчиво к сбоям: ошибка по одной базе не роняет весь бэкап, а логируется как предупреждение.

#!/usr/bin/env bash
set -euo pipefail

#######################################
# ПУТЬ К КОНФИГУ
#######################################
CONFIG_FILE="/etc/backup.conf"

if [ ! -f "$CONFIG_FILE" ]; then
  echo "Ошибка: конфигурационный файл $CONFIG_FILE не найден!" >&2
  exit 1
fi

# shellcheck disable=SC1090
source "$CONFIG_FILE"

#######################################
# ПРОВЕРКА КРИТИЧЕСКИХ НАСТРОЕК
#######################################
if ! declare -p SOURCE_DIRS >/dev/null 2>&1 || [ "${#SOURCE_DIRS[@]}" -eq 0 ]; then
  echo "Ошибка: в $CONFIG_FILE не задан массив SOURCE_DIRS или он пуст." >&2
  exit 1
fi

if [ -z "${REMOTE_PASSWORD:-}" ]; then
  echo "Ошибка: в $CONFIG_FILE не задан REMOTE_PASSWORD." >&2
  exit 1
fi

if [ -z "${REMOTE_BASE_DIR:-}" ]; then
  echo "Ошибка: в $CONFIG_FILE не задан REMOTE_BASE_DIR." >&2
  exit 1
fi

# Каталог под дампы; по умолчанию /opt/backups/db
DB_DUMP_DIR="${DB_DUMP_DIR:-/opt/backups/db}"

LOG_DIR="$(dirname "$LOG_FILE")"
mkdir -p "$LOG_DIR"
touch "$LOG_FILE"
chmod 600 "$LOG_FILE" || true

#######################################
# ЛОГИРОВАНИЕ
#######################################
log() {
  local level="$1"
  local msg="$2"
  echo "$(date +'%Y-%m-%d %H:%M:%S') [${level}] ${msg}" | tee -a "$LOG_FILE"
}

#######################################
# ПРОВЕРКА ЗАВИСИМОСТЕЙ
#######################################
for cmd in tar sftp hostname sshpass; do
  if ! command -v "$cmd" >/dev/null 2>&1; then
    log "ERROR" "Команда '$cmd' не найдена. Установите её и повторите запуск."
    exit 1
  fi
done

#######################################
# ДАМП ВСЕХ БАЗ ДАННЫХ В DOCKER (через сеть, без docker exec)
# Логин/пароль/сеть читаются из метаданных контейнера (docker inspect).
# Дамп снимается одноразовым контейнером-клиентом той же версии образа,
# подключённым к сети базы. Это обходит ограничения seccomp/exec.
# Сбой по одной базе не прерывает бэкап — только предупреждение в лог.
#######################################
if command -v docker >/dev/null 2>&1; then
  log "INFO" "Снятие дампов баз данных в Docker -> ${DB_DUMP_DIR}"
  mkdir -p "$DB_DUMP_DIR"
  DB_STAMP="$(date +%F_%H%M)"

  # достаёт значение переменной окружения VAR из контейнера (через inspect, не exec)
  get_env() {
    docker inspect "$1" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null \
      | sed -n "s/^$2=//p" | head -n1
  }
  # имя первой docker-сети контейнера
  get_net() {
    docker inspect "$1" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{println}}{{end}}' 2>/dev/null | head -n1
  }

  while IFS=$'\t' read -r dbname dbimg; do
    [ -z "$dbname" ] && continue
    low="$(echo "$dbimg" | tr '[:upper:]' '[:lower:]')"
    out="${DB_DUMP_DIR}/${dbname}_${DB_STAMP}"

    # тип базы определяем по имени образа; всё прочее пропускаем
    case "$low" in
      *postgres*|*timescale*|*pgvector*|*mysql*|*mariadb*|*percona*|*mongo*|*redis*|*valkey*|*keydb*) ;;
      *) continue ;;
    esac

    net="$(get_net "$dbname")"
    if [ -z "$net" ]; then
      log "WARN" "  ${dbname}: не определена docker-сеть, пропуск"
      continue
    fi

    case "$low" in
      *postgres*|*timescale*|*pgvector*)
        puser="$(get_env "$dbname" POSTGRES_USER)"; [ -z "$puser" ] && puser="postgres"
        ppass="$(get_env "$dbname" POSTGRES_PASSWORD)"
        if docker run --rm --network "$net" -e PGPASSWORD="$ppass" "$dbimg" \
              pg_dumpall -h "$dbname" -U "$puser" > "${out}.sql" 2>/dev/null && [ -s "${out}.sql" ]; then
          gzip -f "${out}.sql"
          log "INFO" "  [pg] ${dbname} ok (user=${puser})"
        else
          rm -f "${out}.sql"
          log "WARN" "  [pg] ${dbname} НЕ УДАЛОСЬ — проверьте доступы/сеть"
        fi
        ;;
      *mysql*|*mariadb*|*percona*)
        muser="root"
        mpass="$(get_env "$dbname" MYSQL_ROOT_PASSWORD)"; [ -z "$mpass" ] && mpass="$(get_env "$dbname" MARIADB_ROOT_PASSWORD)"
        if docker run --rm --network "$net" "$dbimg" \
              sh -c "exec mysqldump --all-databases -h \"$dbname\" -u\"$muser\" -p\"$mpass\"" > "${out}.sql" 2>/dev/null && [ -s "${out}.sql" ]; then
          gzip -f "${out}.sql"
          log "INFO" "  [mysql] ${dbname} ok"
        else
          rm -f "${out}.sql"
          log "WARN" "  [mysql] ${dbname} НЕ УДАЛОСЬ — проверьте пароль root/сеть"
        fi
        ;;
      *mongo*)
        muser="$(get_env "$dbname" MONGO_INITDB_ROOT_USERNAME)"
        mpass="$(get_env "$dbname" MONGO_INITDB_ROOT_PASSWORD)"
        if [ -n "$muser" ]; then
          auth="--username=$muser --password=$mpass --authenticationDatabase=admin"
        else
          auth=""
        fi
        if docker run --rm --network "$net" "$dbimg" \
              sh -c "exec mongodump --host \"$dbname\" $auth --archive --gzip" > "${out}.archive.gz" 2>/dev/null && [ -s "${out}.archive.gz" ]; then
          log "INFO" "  [mongo] ${dbname} ok"
        else
          rm -f "${out}.archive.gz"
          log "WARN" "  [mongo] ${dbname} НЕ УДАЛОСЬ — проверьте доступы/сеть"
        fi
        ;;
      *redis*|*valkey*|*keydb*)
        rpass="$(get_env "$dbname" REDIS_PASSWORD)"
        authflag=""; [ -n "$rpass" ] && authflag="-a $rpass"
        if docker run --rm --network "$net" "$dbimg" \
              sh -c "redis-cli -h \"$dbname\" $authflag SAVE" >/dev/null 2>&1; then
          log "INFO" "  [redis] ${dbname} снимок на диск (уедет с томом)"
        else
          log "WARN" "  [redis] ${dbname} SAVE не прошёл"
        fi
        ;;
    esac
  done < <(docker ps --format '{{.Names}}\t{{.Image}}')

  # опись окружения — пригодится при восстановлении
  docker ps -a --format '{{.Names}}\t{{.Image}}\t{{.Status}}' > "${DB_DUMP_DIR}/_containers.txt" 2>/dev/null || true
  docker images --format '{{.Repository}}:{{.Tag}}'           > "${DB_DUMP_DIR}/_images.txt"     2>/dev/null || true

  log "INFO" "Дампы баз готовы в ${DB_DUMP_DIR}"
else
  log "WARN" "docker не найден — пропускаю дамп баз, архивирую только файлы"
fi

#######################################
# ГЕНЕРАЦИЯ ИМЁН И ПУТЕЙ
#######################################
HOSTNAME_LOCAL="$(hostname -f)"
DATE_NOW="$(date +'%Y-%m-%d')"
TIME_NOW="$(date +'%H-%M-%S')"

BACKUP_NAME="backup_${DATE_NOW}_${TIME_NOW}.tar.gz"
LOCAL_BACKUP_PATH="${LOCAL_TMP_DIR}/${BACKUP_NAME}"
REMOTE_SUBDIR="${HOSTNAME_LOCAL}/${DATE_NOW}"
REMOTE_FULL_PATH="${REMOTE_BASE_DIR}/${REMOTE_SUBDIR}"

#######################################
# ПРОВЕРКА ЛОКАЛЬНЫХ ДИРЕКТОРИЙ
#######################################
for dir in "${SOURCE_DIRS[@]}"; do
  if [ ! -d "$dir" ]; then
    log "ERROR" "Директория '${dir}' не существует"
    exit 1
  fi
done

#######################################
# СОЗДАНИЕ АРХИВА
# --warning=no-file-changed гасит ложные ошибки при изменении файлов
# работающими контейнерами во время чтения.
#######################################
log "INFO" "Запуск бэкапа: ${SOURCE_DIRS[*]} (хост: ${HOSTNAME_LOCAL})"
log "INFO" "Создаю архив ${LOCAL_BACKUP_PATH}"

tar --warning=no-file-changed --ignore-failed-read -czf "$LOCAL_BACKUP_PATH" "${SOURCE_DIRS[@]}" || {
  log "WARN" "Архивация завершилась с предупреждениями (изменялись файлы), продолжение допускается"
}

if [ ! -f "$LOCAL_BACKUP_PATH" ]; then
  log "ERROR" "Архив ${LOCAL_BACKUP_PATH} не найден после архивации"
  exit 1
fi

#######################################
# ОТПРАВКА ЧЕРЕЗ SFTP (sshpass + here-doc)
# Ошибка mkdir на уже существующем каталоге — нормально и не влияет на put.
#######################################
log "INFO" "Отправка архива на ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_FULL_PATH}"

if ! sshpass -p "$REMOTE_PASSWORD" sftp \
    -o PreferredAuthentications=password \
    -o PubkeyAuthentication=no \
    -o StrictHostKeyChecking=accept-new \
    -P "$REMOTE_PORT" \
    "${REMOTE_USER}@${REMOTE_HOST}" >>"$LOG_FILE" 2>&1 <<EOF
cd ${REMOTE_BASE_DIR}
mkdir ${HOSTNAME_LOCAL}
mkdir ${HOSTNAME_LOCAL}/${DATE_NOW}
cd ${HOSTNAME_LOCAL}/${DATE_NOW}
put ${LOCAL_BACKUP_PATH}
EOF
then
  log "ERROR" "Передача по SFTP завершилась с ошибкой, подробности в логе: ${LOG_FILE}"
  rm -f "$LOCAL_BACKUP_PATH"
  exit 1
fi

rm -f "$LOCAL_BACKUP_PATH"
log "INFO" "Архив успешно отправлен и локальный файл удалён: ${BACKUP_NAME}"
log "INFO" "Бэкап завершён успешно: ${REMOTE_FULL_PATH}/${BACKUP_NAME}"
exit 0

Как скрипт определяет тип базы

Скрипт не привязан к заранее заданному списку имён контейнеров. Он перебирает все запущенные контейнеры (docker ps) и определяет тип СУБД по имени образа. Поднимется новый контейнер с MariaDB или MongoDB — скрипт подхватит его сам, без правок:

  • образ содержит postgres, timescale или pgvectorpg_dumpall;
  • mysql, mariadb или perconamysqldump --all-databases;
  • mongomongodump --archive --gzip;
  • redis, valkey или keydbredis-cli SAVE;
  • всё остальное → не база, уедет файлами в составе SOURCE_DIRS.

Дамп снимается по сети, а не через docker exec. Это ключевое архитектурное решение. Для каждой базы скрипт поднимает одноразовый контейнер той же версии образа (docker run --rm), подключает его к той же docker-сети, что и сама база, и обращается к ней по TCP как обычный сетевой клиент. Хостнеймом служит имя контейнера базы — внутри docker-сети контейнеры резолвятся по именам.

У такого подхода два важных преимущества перед docker exec. Во-первых, версия клиента (pg_dumpall, mysqldump) всегда совпадает с версией сервера, потому что образ берётся один и тот же. Во-вторых, он не зависит от состояния docker exec в конкретном контейнере: на некоторых хостах exec не проходит из-за рассогласования профиля seccomp с ядром (ошибка вида SetSSB requires libseccomp >= 2.5.0 and API level >= 4), и сетевой способ это ограничение обходит, не требуя правки системы.

Доступы читаются из метаданных, а не угадываются

Пользователь, пароль и имя сети скрипт достаёт через docker inspect — он работает снаружи контейнера и от seccomp не зависит. Для PostgreSQL берётся POSTGRES_USER (если не задан — postgres) и POSTGRES_PASSWORD; для MySQL/MariaDB — MYSQL_ROOT_PASSWORD или MARIADB_ROOT_PASSWORD; для MongoDB — MONGO_INITDB_ROOT_USERNAME/PASSWORD. Никакого перебора имён: поднимется база с любым нестандартным пользователем — он подхватится автоматически.

Redis

У Redis нет логического дампа в привычном смысле — команда SAVE синхронно сбрасывает снимок памяти в файл dump.rdb внутри контейнера. Этот файл лежит в томе данных и уедет в архив вместе с /var/lib/docker/volumes. То есть для Redis скрипт обеспечивает свежесть снимка, а сам файл едет файловой частью.

Ограничение. Способ рассчитан на базы, подключённые к docker-сети (типичный случай для compose-стеков). Если контейнер запущен в режиме network_mode: host или вообще без сети, имя сети не определится — скрипт залогирует пропуск и перейдёт к следующей базе, не прерывая бэкап.

Защита от битых дампов

После каждого дампа проверяется, что файл непустой ([ -s ... ]). Пустой файл от неудачной попытки удаляется, чтобы в архив не уехала «обманка» нулевого размера, которая при восстановлении выглядела бы как существующая, но пустая база. Это единственное удаление, которое делает блок дампа, — накопленные дампы он не трогает.

Опись окружения

Рядом с дампами скрипт кладёт два файла: _containers.txt (что было запущено и какими образами) и _images.txt (полный список образов с тегами). При восстановлении на новом сервере по ним сразу видно, какие версии контейнеров поднимать.

Что входит в SOURCE_DIRS и почему

Минимально разумный набор для сервера с Docker:

  • /etc — системные конфиги, в том числе /etc/docker/daemon.json;
  • /home — данные проектов, если они лежат в домашних каталогах;
  • /opt — код проектов (compose-файлы, Dockerfile, .env) и сами дампы баз из DB_DUMP_DIR;
  • /var/lib/docker/volumes — все именованные тома Docker (данные сервисов, которые не вынесены в bind-монтирования).

Если данные сервисов лежат вне этого набора (например, в /srv или другом каталоге, заданном при развёртывании), такой путь добавляется в SOURCE_DIRS отдельно. Некоторые инструменты управления контейнерами (например, Portainer) складывают данные стеков в собственный каталог вроде /data/compose — стоит проверить, где именно, и при необходимости включить его в список.

Чего здесь намеренно нет — /var/lib/docker/overlay2 (слои образов, гигабайты) и /var/lib/docker/image. Их бэкапить незачем: образы заново тянутся из реестра или пересобираются из Dockerfile, а какие именно теги поднимать — подскажет _images.txt.

Как найти каталоги данных, которые торчат за пределами этого набора (bind-монтирования в нестандартные пути): пройтись по выводу docker inspect и проверить левые части всех volumes. Любой абсолютный путь, не начинающийся на каталоги из SOURCE_DIRS, — это дыра, которую нужно либо добавить в список, либо учесть отдельно.

Права, chroot и проверка каталога на SFTP

Перед включением в cron проверяется вручную, что SFTP-пользователь имеет доступ на запись в REMOTE_BASE_DIR. Многие хранилища сажают SFTP-пользователя в chroot, и абсолютные пути вида /backup могут оказаться недоступны:

sftp -P 22 sftp_user@<IP>
sftp> pwd          # стартовая директория (с учётом chroot)
sftp> ls
sftp> mkdir test   # проверка прав на запись
sftp> rmdir test
sftp> exit

Если mkdir даёт Permission denied — для REMOTE_BASE_DIR нужен другой каталог. Если pwd показывает, что вы уже внутри нужной области, — используется относительный путь (backup), а не абсолютный.

Про mkdir в самом скрипте: в SFTP-сессии команды mkdir для каталогов хоста и даты выполняются каждый запуск. Со второго дня каталог хоста уже существует, и mkdir по нему вернёт ошибку — это нормально и на загрузку файла не влияет, потому что результат сессии оценивается по общему коду выхода, а не покомандно.

Про пароль и альтернативу — SSH-ключ

Хранение пароля в backup.conf открытым текстом допустимо только при правах 600 на файл и ограниченном SFTP-пользователе. Более надёжный вариант — аутентификация по SSH-ключу вместо пароля и sshpass.

Коротко, как перейти на ключ:

# 1. Сгенерировать ключ без пароля (для автоматического запуска)
ssh-keygen -t ed25519 -f /root/.ssh/backup_key -N ""

# 2. Передать публичный ключ на SFTP-сервер
#    (если есть SSH-доступ — ssh-copy-id; если только SFTP —
#     добавьте содержимое backup_key.pub в authorized_keys
#     SFTP-пользователя через панель хранилища)

После этого в скрипте sshpass -p "$REMOTE_PASSWORD" sftp ... заменяется на sftp -i /root/.ssh/backup_key ..., а PreferredAuthentications меняется на publickey. Пароль из конфига убирается совсем. Для серверов, доступных из интернета, это предпочтительный способ.

Автоматизация через cron

После ручной проверки добавляется задание в crontab пользователя root (нужны права на чтение всех резервируемых каталогов и на docker):

sudo crontab -e

Ежедневный запуск в 03:00:

0 3 * * * PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin /usr/local/bin/backup.sh

PATH задан явно, чтобы в окружении cron гарантированно нашлись tar, sftp, sshpass и docker. Полный путь до скрипта исключает ошибки со сменой рабочего каталога.

Проверка восстановления

Бэкап, который ни разу не разворачивали, — это не бэкап, а надежда на него.

Минимум раз в месяц стоит проверять, что архив распаковывается и дампы внутри читаются. Базовая проверка, что дампы вообще попали в архив:

tar tzf backup_2026-06-27_03-00-00.tar.gz | grep backups/db

Восстановление базы PostgreSQL из дампа сводится к подъёму чистого контейнера нужной версии и заливке .sql:

# распаковали дамп нужной базы
gunzip app-db-1_2026-06-27_0300.sql.gz

# залили в свежий контейнер postgres той же мажорной версии
cat app-db-1_2026-06-27_0300.sql | docker exec -i app-db-1 psql -U postgres

Если docker exec на хосте не проходит (та же проблема с seccomp), заливать можно по сети — одноразовым клиентом в сети базы, симметрично тому, как снимался дамп:

docker run --rm -i --network "$NET" -e PGPASSWORD='ПАРОЛЬ' postgres:16 \
    psql -h app-db-1 -U postgres < app-db-1_2026-06-27_0300.sql

Для MySQL аналогично — mysql -uroot -p < dump.sql; для MongoDB — mongorestore --archive --gzip. Версия контейнера базы при восстановлении должна совпадать по мажорному номеру с той, что снимала дамп (её видно в _images.txt).

FAQ

Можно ли использовать этот скрипт на сервере без баз данных?

Да. Если контейнеров с базами нет, блок дампа просто ничего не найдёт и сразу перейдёт к архивации файлов — скрипт отработает как обычный файловый бэкап по SFTP. Если же docker на сервере вообще не установлен, блок дампа пропускается с предупреждением в лог.

Нужно ли ставить клиенты баз (pg_dump, mysqldump) на хост?

Нет. Для каждой базы скрипт поднимает одноразовый контейнер той же версии образа и снимает дамп его клиентом по сети. Нужный инструмент приходит вместе с образом базы, на хосте нужен только сам docker.

Почему дамп снимается по сети, а не через docker exec?

Сетевой способ надёжнее в двух отношениях. Версия клиента всегда совпадает с версией сервера (образ один и тот же), и он не зависит от состояния docker exec в контейнере — на некоторых хостах exec не проходит из-за рассогласования профиля seccomp с ядром, а подключение по сети это обходит без правки системы.

Скрипт упал на одной базе — потеряю ли я весь бэкап?

Нет. Сбой по отдельной базе логируется как WARN, и скрипт продолжает работу: дампит остальные базы и архивирует файлы. В лог попадёт строка НЕ УДАЛОСЬ с именем проблемной базы — это сигнал проверить её доступы вручную.

Дампит ли скрипт базы, установленные прямо в систему, а не в Docker?

Нет. Цикл проходит только по контейнерам Docker. Если на сервере есть СУБД на «голом железе» (из apt/dnf), для неё нужно добавить отдельный вызов дампа перед блоком архивации.

Почему дамп складывается в /opt, а не в отдельный каталог?

Чтобы дампы автоматически попали в архив, DB_DUMP_DIR должен лежать внутри одного из SOURCE_DIRS. /opt уже в списке, поэтому дампы уезжают вместе с остальными файлами без отдельной настройки. Каталог можно сменить — главное, чтобы он оставался внутри архивируемых путей.

Как добавить поддержку базы, которой нет в списке?

Дописать ветку в конструкцию case по образу контейнера, вызвав штатный инструмент дампа этой СУБД. Структура остальных веток послужит образцом.

Можно ли шифровать архив перед отправкой?

Да. Перед блоком отправки по SFTP добавляется шаг шифрования (например, через GPG), и отправляется уже зашифрованный файл. Это разумно для чувствительных данных, уезжающих на чужое хранилище.

Заключение

Связка backup.sh + backup.conf закрывает полный цикл резервного копирования Linux-сервера с Docker: находит все базы в контейнерах, снимает согласованные дампы штатными инструментами, архивирует конфиги, данные приложений и тома вместе с дампами и отправляет всё одним архивом на изолированный SFTP-сервер. Решение переиспользуется на разных серверах через один конфиг, корректно работает в окружениях с chroot-ограничениями, а проверка DB_DUMP_DIR внутри SOURCE_DIRS, права 600 на конфиг и ежемесячное тестовое восстановление превращают его в production-ready элемент устойчивости инфраструктуры — именно согласованность баз данных чаще всего отличает копию, из которой можно восстановиться, от копии, которая лишь выглядит таковой.

Оцените статью
ctrllife.ru
Подписаться
Уведомить о
guest
0 комментариев
Старые
Новые Популярные
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x