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

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

MAATRIX

Если бэкап сервера сейчас держится на самописном bash-скрипте с закодированным паролем внутри и cron-строкой, которую страшно трогать, — знакомая ситуация. Restic решает эту задачу иначе: дедупликация «из коробки», шифрование на клиенте и один бинарник, который одинаково работает с локальным диском, SFTP и S3. Ниже — рабочий docker-compose.yml для всех трёх бэкендов и то, как заставить контейнерный restic бэкапиться по расписанию, а не только руками.

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

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

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

Почему restic и почему в контейнере

Restic — это дедуплицирующий бэкап-инструмент: файлы режутся на чанки переменного размера по содержимому (content-defined chunking), и одинаковые куски данных между снимками хранятся один раз. На практике это значит, что после первого полного бэкапа последующие снимки занимают в хранилище заметно меньше места, чем сами данные — точный процент экономии зависит от того, насколько данные между снимками похожи, конкретных цифр без замера на своих данных давать не буду.

Шифрование в restic — на стороне клиента, AES-256, и происходит до отправки данных в хранилище. Это значит, что бэкенду (диску, SFTP-серверу, S3-бакету) не обязательно доверять: он видит только зашифрованные блобы. Ключ шифрования отдельный от бэкенда, и без пароля репозиторий не открывается никаким штатным способом — это разобрано подробнее в статье про частые ошибки бэкапа с шифрованием.

Зачем заворачивать это в Docker Compose, если restic и так один бинарник:

  • Версия фиксирована. image: restic/restic:0.16.4 — и на новом сервере поднимается ровно та же версия, а не то, что подтянул apt на момент установки.
  • Конфигурация лежит рядом с приложением. Один docker-compose.yml для сервиса и его бэкапа, без отдельного скрипта где-то в /opt.
  • Секреты не разбросаны по shell-истории. Пароль репозитория и ключи бэкенда идут через secrets:, а не через export в скрипте, который кто-то потом закоммитит в git по ошибке.

Минус тоже есть: restic — не демон, он завершает работу после каждой команды. Контейнер, который просто «крутится», тут не подойдёт по умолчанию — про это ниже, в разделе про расписание.

Готовый docker-compose.yml: локальный диск

Самый простой вариант — бэкап на отдельный смонтированный диск или раздел этого же сервера. Он не заменяет полноценный офсайт-бэкап (диск может умереть вместе с сервером), но годится как первый шаг или как одна из копий по правилу «3-2-1».

services:
  restic:
    image: restic/restic:latest
    container_name: restic-backup
    environment:
      RESTIC_REPOSITORY: /repo
      RESTIC_PASSWORD_FILE: /run/secrets/restic_password
    volumes:
      - /srv/app/data:/data:ro
      - /mnt/backup-disk/restic-repo:/repo
      - restic-cache:/root/.cache/restic
    secrets:
      - restic_password
    command: backup /data
    restart: "no"

secrets:
  restic_password:
    file: ./secrets/restic_password.txt

volumes:
  restic-cache:

Данные монтируются в /data только на чтение — контейнеру-бэкаперу незачем иметь право что-то там менять. Каталог /root/.cache/restic вынесен в отдельный volume: без него restic будет заново скачивать индексы репозитория при каждом запуске контейнера, что заметно замедляет и локальный, и особенно удалённый бэкенд.

Репозиторий нужно инициализировать один раз до первого бэкапа:

docker compose run --rm restic init

После этого docker compose run --rm restic (с командой из файла или переопределённой) выполняет бэкап. restart: "no" явный, хотя это и значение по умолчанию — так понятнее, что сервис не должен «висеть», это одноразовая задача, а не долгоживущий процесс.

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

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

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

Бэкенд S3 и S3-совместимые хранилища

Меняется только RESTIC_REPOSITORY и способ передать доступ к объектному хранилищу. Restic понимает как AWS S3, так и совместимые с S3 API хранилища (MinIO, Backblaze B2 через совместимый эндпоинт и другие) — для этого в адрес репозитория подставляется свой хост.

services:
  restic:
    image: restic/restic:latest
    container_name: restic-backup
    environment:
      RESTIC_REPOSITORY: s3:https://s3.eu-west-1.amazonaws.com/my-backup-bucket/app-data
      RESTIC_PASSWORD_FILE: /run/secrets/restic_password
      AWS_SHARED_CREDENTIALS_FILE: /run/secrets/aws_credentials
    volumes:
      - /srv/app/data:/data:ro
      - restic-cache:/root/.cache/restic
    secrets:
      - restic_password
      - aws_credentials
    command: backup /data
    restart: "no"

secrets:
  restic_password:
    file: ./secrets/restic_password.txt
  aws_credentials:
    file: ./secrets/aws_credentials.ini

volumes:
  restic-cache:

Файл aws_credentials.ini — обычный формат credentials-файла AWS SDK:

[default]
aws_access_key_id = ВАШ_КЛЮЧ
aws_secret_access_key = ВАШ_СЕКРЕТ

Для S3-совместимого хранилища (например, MinIO на своём сервере) репозиторий выглядит так же, только с адресом своего эндпоинта: s3:https://minio.example.com/backup-bucket/app-data. Если MinIO поднят без валидного TLS-сертификата, потребуется дополнительно указать RESTIC_CACERT с путём до CA-сертификата — иначе restic откажется соединяться по HTTPS с самоподписанным сертификатом.

Про то, зачем вообще прятать такие ключи через secrets:, а не через environment: в открытом виде, подробно — в статье Docker secrets: управление паролями.

Бэкенд SFTP: свой сервер как хранилище

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

services:
  restic:
    image: restic/restic:latest
    container_name: restic-backup
    environment:
      RESTIC_REPOSITORY: sftp:backup@203.0.113.10:/mnt/restic-repo
      RESTIC_PASSWORD_FILE: /run/secrets/restic_password
    volumes:
      - /srv/app/data:/data:ro
      - restic-cache:/root/.cache/restic
      - ~/.ssh/id_ed25519_backup:/root/.ssh/id_ed25519:ro
      - ~/.ssh/known_hosts:/root/.ssh/known_hosts:ro
    secrets:
      - restic_password
    command: backup /data
    restart: "no"

secrets:
  restic_password:
    file: ./secrets/restic_password.txt

volumes:
  restic-cache:

Бэкенд SFTP у restic работает через реальный sftp-клиент (часть openssh-client), поэтому важны две вещи: приватный ключ должен быть смонтирован с правильными правами (если на хосте у файла ключа права шире, чем 600, ssh может отказаться его использовать), а known_hosts — уже содержать отпечаток удалённого сервера, иначе первое подключение из неинтерактивного контейнера зависнет на вопросе о доверии хосту. Отпечаток проще всего добавить заранее командой ssh-keyscan 203.0.113.10 >> ~/.ssh/known_hosts на хосте.

Ключ SSH стоит генерировать отдельный, только для бэкапа, с ограничением на нужный каталог через command= в authorized_keys — тогда даже утечка ключа не даст доступа ко всему серверу-хранилищу.

Расписание: backup и forget без демона в контейнере

Restic не остаётся жить после команды, поэтому restart: unless-stopped на сервисе выше ничего не даст — контейнер просто будет постоянно перезапускаться и сразу завершаться. Рабочих вариантов два.

Проще и надёжнее — cron на хосте, который дёргает docker compose run:

0 3 * * * cd /srv/app && docker compose run --rm restic backup /data >> /var/log/restic-backup.log 2>&1
30 3 * * 0 cd /srv/app && docker compose run --rm restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune >> /var/log/restic-forget.log 2>&1

Ротация через forget с флагом --prune обязательна отдельной задачей: без неё репозиторий растёт бесконечно, даже с дедупликацией — снимки просто помечаются старыми, но место физически не освобождается, пока не пройдёт prune.

Если хочется, чтобы расписание тоже жило в Docker — добавьте ofelia, планировщик задач для контейнеров. Держите restic живым как idle-контейнер (entrypoint: sleep infinity вместо команды бэкапа) и запускайте в нём команды по docker exec через ofelia:

services:
  restic:
    image: restic/restic:latest
    container_name: restic
    entrypoint: sleep infinity
    environment:
      RESTIC_REPOSITORY: /repo
      RESTIC_PASSWORD_FILE: /run/secrets/restic_password
    volumes:
      - /srv/app/data:/data:ro
      - /mnt/backup-disk/restic-repo:/repo
      - restic-cache:/root/.cache/restic
    secrets:
      - restic_password
    restart: unless-stopped

  ofelia:
    image: mcuadros/ofelia:latest
    depends_on:
      - restic
    command: daemon --docker
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    labels:
      ofelia.job-exec.restic-backup.schedule: "0 3 * * *"
      ofelia.job-exec.restic-backup.container: restic
      ofelia.job-exec.restic-backup.command: "restic backup /data"
      ofelia.job-exec.restic-forget.schedule: "30 3 * * 0"
      ofelia.job-exec.restic-forget.container: restic
      ofelia.job-exec.restic-forget.command: "restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune"
    restart: unless-stopped

secrets:
  restic_password:
    file: ./secrets/restic_password.txt

volumes:
  restic-cache:

sleep infinity — не элегантно, но честно: это способ держать контейнер живым как цель для docker exec, сам restic по-прежнему не демон. Перед использованием сверьтесь с документацией ofelia — параметры job-exec могут отличаться между версиями образа.

Сравнение бэкендов по тому, что нужно контейнеру:

БэкендRESTIC_REPOSITORYЧто нужно контейнеруКогда уместен
Локальный диск/repovolume с точкой монтирования отдельного дискаПервый шаг или одна из копий правила 3-2-1, не единственная
SFTPsftp:user@host:/pathприватный ключ, готовый known_hostsЕсть свой второй сервер в другой локации, нужен полный контроль над хранилищем
S3 / совместимоеs3:https://endpoint/bucket/pathcredentials-файл AWS SDKManaged-хранилище без администрирования ещё одного сервера

Восстановление и проверка бэкапа

Бэкап, который никогда не проверялся на восстановление, — это не бэкап, а надежда. Три команды, которые стоит гонять регулярно, а не только в момент катастрофы:

docker compose run --rm restic snapshots
docker compose run --rm restic check
docker compose run --rm -v /tmp/restore-test:/restore restic restore latest --target /restore

snapshots показывает, что копии вообще создаются и с ожидаемой периодичностью. check проверяет целостность репозитория на уровне метаданных и (с флагом --read-data-subset время от времени) целостность самих данных. Тестовое восстановление в отдельный каталог — единственный способ по-настоящему убедиться, что из репозитория можно достать рабочие файлы, а не повреждённые фрагменты. Общий разбор типовых проблем с восстановлением и потерянными паролями — в статье частые ошибки бэкапа с шифрованием, там же — почему нельзя держать репозиторий на том же диске, что и данные.

Если бэкапится не файловая система приложения, а том Docker с базой данных, восстановление устроено чуть иначе — сначала останавливается сервис, потом заменяется содержимое volume, и здесь есть свои грабли с правами и активными подключениями к базе, они разобраны в статье бэкап Docker volume: частые ошибки и решения.

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

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

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

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

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

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

Restic сам создаёт репозиторий при первом бэкапе?

Нет, нужно инициализировать явно командой restic init (в контейнере — docker compose run --rm restic init) до первого backup. Иначе backup завершится ошибкой, что репозиторий не существует.

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

Да, проще всего — отдельные сервисы в одном compose-файле с разными volumes: и разными путями в RESTIC_REPOSITORY, чтобы снимки разных приложений не перемешивались.

Что будет, если контейнер с бэкапом упадёт посреди работы?

Restic оставит блокировку на репозитории — следующий запуск откажется работать. Если вы уверены, что активной операции нет, блокировка снимается командой restic unlock.

Нужен ли восстановлению тот же образ restic, что делал бэкап?

Формат репозитория обратно совместим между минорными версиями, но безопаснее фиксировать тег (restic/restic:0.16.4, а не latest) — тогда версия предсказуема.

Кеш-том обязателен?

Нет, но без него каждый запуск заново тянет индексы репозитория с бэкенда — для S3 и SFTP это заметно увеличивает время и трафик, особенно на репозитории с большим числом снимков.

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

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

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