MAATRIX / Блог / BorgBackup в Docker Compose: готовый файл

BorgBackup в Docker Compose: готовый файл

MAATRIX

Бэкапы «на всякий случай» обычно живут в виде голого tar.gz в cron, который никто давно не проверял — и вскрываются в момент, когда сервер уже упал. BorgBackup решает конкретную проблему: он дедуплицирует данные на уровне блоков, сжимает их и хранит десятки версий в объёме, который для tar был бы неподъёмным. Ниже — рабочий docker-compose.yml, который поднимает Borg как сервис, а не как разовую команду, которую забудут перезапустить после ребута.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Почему Borg, а не tar+rsync

Классический вариант «упаковать в tar.gz и залить по rsync» работает, но у него два системных недостатка. Во-первых, каждый полный архив занимает столько же места, сколько исходные данные — инкрементальность приходится городить руками через --link-dest или отдельные скрипты. Во-вторых, при росте базы файлов копирование становится всё дольше: rsync каждый раз обходит дерево целиком.

Borg устроен иначе: он режет данные на чанки переменного размера, считает хэш каждого чанка и хранит только уникальные блоки. Если вы бэкапите 50 ГБ, из которых за сутки изменилось 200 МБ, новый снапшот в репозитории займёт примерно эти 200 МБ (плюс метаданные), а не все 50 ГБ заново. При этом каждый снапшот в интерфейсе borg list выглядит как полноценный, самостоятельный архив — восстановить можно любую точку без «накатывания» цепочки инкрементов.

ИнструментДедупликацияКомпрессияШифрованиеИнкрементальность
tar.gz + cronнетда (gzip)нет (нужен gpg отдельно)нет, каждый раз полный архив
rsync (снапшоты через hardlink)частично (на уровне файлов)нетнетда
BorgBackupда, на уровне блоковда (lz4/zstd/zlib)да, встроенноеда, прозрачная
resticда, на уровне блоковдадада

Restic — ближайший конкурент Borg, у него удобнее работа с облачными бэкендами (S3, B2) из коробки. Borg исторически сильнее в SSH-репозиториях и compaction — плюс формат репозитория стабильнее живёт годами без миграций. Для «одного сервера, один диск с бэкапами (свой или арендованный)» разница небольшая, я использую Borg просто по привычке и из-за borg mount — репозиторий можно примонтировать как FUSE-файловую систему и полистать файлы без полного extract.

Готовый docker-compose.yml

Сервис собирается из своего образа — заранее собранных официальных образов borgbackup под Docker Hub нет, а тянуть чужой community-образ ради одного бинарника избыточно. Ниже — минимальный, но рабочий вариант.

# docker-compose.yml
services:
  borgbackup:
    build: ./borg
    container_name: borgbackup
    restart: unless-stopped
    environment:
      BORG_REPO: /repo
      BORG_PASSPHRASE_FILE: /run/secrets/borg_passphrase
      TZ: Europe/Moscow
    volumes:
      # источники бэкапа — монтируем read-only
      - /var/lib/docker/volumes/app_pgdata/_data:/sources/pgdata:ro
      - /opt/app/uploads:/sources/uploads:ro
      - /etc/nginx:/sources/nginx-conf:ro
      # куда пишет borg (лучше отдельный диск/раздел)
      - /mnt/backup-disk/borg-repo:/repo
      # скрипт и конфиг
      - ./borg/backup.sh:/backup.sh:ro
      - ./borg/crontab:/etc/crontabs/root:ro
    secrets:
      - borg_passphrase
    command: crond -f -l 2

secrets:
  borg_passphrase:
    file: ./borg/passphrase.txt

Passphrase лежит в файле, а не в переменной окружения контейнера — так она не всплывёт в docker inspect и логах. passphrase.txt добавьте в .gitignore, права chmod 600. Если репозиторий у вас уже вынесен на отдельный сервер бэкапов — те же принципы работы с паролями и ключами разобраны в статье про бэкап с шифрованием на VPS.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Dockerfile и скрипт бэкапа

Образ — Alpine с пакетом borgbackup и openssh-client на случай, если репозиторий будет удалённым.

# borg/Dockerfile
FROM alpine:3.20
RUN apk add --no-cache borgbackup openssh-client tzdata
WORKDIR /
ENTRYPOINT []

Сам бэкап — не однострочник borg create, а скрипт с обработкой ошибок и логом, иначе через полгода вы узнаете, что бэкапы падают, только в момент восстановления.

#!/bin/sh
# borg/backup.sh
set -e

export BORG_PASSPHRASE="$(cat "$BORG_PASSPHRASE_FILE")"
export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes

REPO="${BORG_REPO:-/repo}"
ARCHIVE_NAME="{hostname}-$(date +%Y-%m-%d_%H-%M)"

echo "[$(date -Iseconds)] старт бэкапа"

borg create \
  --stats \
  --compression zstd,15 \
  --exclude-caches \
  "$REPO::$ARCHIVE_NAME" \
  /sources/pgdata \
  /sources/uploads \
  /sources/nginx-conf

echo "[$(date -Iseconds)] чистка старых архивов"

borg prune \
  --list \
  --keep-daily=7 \
  --keep-weekly=4 \
  --keep-monthly=6 \
  "$REPO"

borg compact "$REPO"

echo "[$(date -Iseconds)] готово"

Флаг --exclude-caches пропускает директории с файлом CACHEDIR.TAG (временные кэши приложений, если они попали в путь бэкапа). Уровень компрессии zstd,15 — компромисс между скоростью и степенью сжатия; для баз данных (уже частично сжатых дампов) разница между уровнями 6 и 15 обычно небольшая, а вот для текстовых конфигов и логов заметная — но точные цифры зависят от ваших данных, ориентируйтесь на собственный тест, а не на чужие бенчмарки.

Инициализация репозитория и шифрование

Репозиторий нужно создать один раз, руками, до первого запуска по расписанию:

docker compose run --rm borgbackup sh -c '
  export BORG_PASSPHRASE="$(cat /run/secrets/borg_passphrase)"
  borg init --encryption=repokey-blake2 "$BORG_REPO"
'

repokey-blake2 — режим, при котором ключ шифрования хранится внутри самого репозитория (зашифрованный passphrase), а не отдельным файлом на клиенте. Это удобнее для одного сервера, но означает: если сгорит диск с репозиторием, ключ сгорит вместе с ним — если у вас есть отдельный офсайт-репозиторий, для него разумнее keyfile-blake2 с ключом, который вы храните отдельно (например, в менеджере паролей). После инициализации обязательно сделайте:

docker compose run --rm borgbackup borg key export "$BORG_REPO" /repo/key-backup.txt

И унесите key-backup.txt вместе с passphrase в третье место — без ключа зашифрованный репозиторий не открывается никем и никогда, это не «забыл пароль от почты», это безвозвратная потеря данных.

Retention: сколько версий хранить

borg prune в скрипте выше держит 7 суточных, 4 недельных и 6 месячных снапшотов — это отправная точка, а не догма. Логика такая: чем чаще меняются данные, тем гуще должна быть суточная сетка; для статики (конфиги, редко правящиеся файлы) можно сократить до --keep-daily=3 --keep-weekly=2.

ПараметрЧто значитПример значения
--keep-dailyсуточных снапшотов7 (неделя)
--keep-weeklyнедельных4 (месяц)
--keep-monthlyмесячных6 (полгода)
--keep-yearlyгодовых1-2, если нужна долгая ретроспектива

Важный нюанс: prune помечает архивы к удалению, но реально освобождает место только compact (в скрипте он идёт следующим шагом). Без compact репозиторий будет расти, даже если prune формально отработал — это частая причина «а почему бэкапы всё равно жрут весь диск» на форумах.

Автоматизация через crontab контейнера

Файл crontab, который монтируется в /etc/crontabs/root, а сам контейнер держит crond на переднем плане (crond -f), поэтому не завершается сразу после старта:

# borg/crontab
0 3 * * * /backup.sh >> /proc/1/fd/1 2>> /proc/1/fd/2

Перенаправление в /proc/1/fd/1 — трюк, чтобы вывод скрипта попадал в docker logs, а не терялся в никуда (у cron внутри контейнера обычно нет своего терминала). Если предпочитаете системный подход без cron внутри контейнера — можно запускать docker compose run --rm borgbackup /backup.sh из systemd-таймера хоста; сравнение вариантов планирования разобрано в статье про настройку cron-задач, а для полностью контейнеризированного стека полезно заглянуть в материал про частые ошибки docker-compose в продакшене.

Восстановление файлов и целых томов

Список архивов и их содержимого:

docker compose run --rm borgbackup borg list "$BORG_REPO"
docker compose run --rm borgbackup borg list "$BORG_REPO::app-2026-08-30_03-00"

Восстановление конкретных файлов без разворачивания всего архива:

docker compose run --rm -v /tmp/restore:/restore borgbackup sh -c '
  cd /restore && borg extract "$BORG_REPO::app-2026-08-30_03-00" sources/pgdata
'

Borg восстанавливает данные с сохранением относительного пути внутри архива, поэтому файлы появятся в /tmp/restore/sources/pgdata. Для точечного просмотра без полной распаковки удобнее смонтировать архив через FUSE:

docker compose run --rm --cap-add SYS_ADMIN --device /dev/fuse \
  -v /tmp/mnt:/mnt borgbackup sh -c '
  borg mount "$BORG_REPO::app-2026-08-30_03-00" /mnt && sleep 300
'

Пока команда «висит» (300 секунд в примере), в соседнем терминале можно зайти в /tmp/mnt и скопировать нужное вручную, не трогая остальное. Отмонтировать — borg umount /tmp/mnt или дождаться таймаута.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли бэкапить работающую базу PostgreSQL напрямую через volume-mount?

Не напрямую — файлы на диске СУБД в момент записи не гарантируют консистентность снапшота. Правильно: сначала pg_dump или pg_basebackup во временный файл, потом Borg архивирует уже готовый дамп. Подробности — в статье про резервное копирование БД на VPS.

Что если диск с репозиторием и диск с данными — один и тот же?

Тогда это не бэкап, а копия для удобства, которая сгорит вместе с оригиналом при отказе диска. Минимум — второй физический диск на том же сервере, разумный вариант — отдельный сервер под бэкапы или объектное хранилище, синхронизируемое через rclone поверх готового Borg-репозитория.

Borg умеет бэкапить сразу на удалённый сервер по SSH?

Да, BORG_REPO может быть вида ssh://user@backup-host/./repo — тогда нужен смонтированный в контейнер SSH-ключ и known_hosts, остальной скрипт не меняется.

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

Периодически (не каждую ночь — операция небыстрая) гоняйте borg check --repository-only "$BORG_REPO", а раз в месяц — borg check --verify-data для полной проверки чанков против чек-сумм.

Чем это лучше готовых панелей вроде Duplicati?

Ничем принципиально — Duplicati удобнее визуально, Borg честнее с форматом репозитория и предсказуемее в CLI-автоматизации. Для connect-and-forget GUI-решения Duplicati может быть проще; для воспроизводимого docker-compose-стека, который переживёт миграцию на новый сервер, я выбираю Borg.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →