Представьте восстановление, которое по всем внешним признакам прошло идеально. Контейнер с базой поднялся, приложение отвечает, список заказов открывается. А потом менеджер открывает заказ и видит: вложено три файла, счёт, макет и фотография. На странице есть три ссылки, а в каталоге uploads лежит только макет. Обе копии, и база, и каталог с файлами, по отдельности «здоровы». Просто сделаны они были в разное время: файлы снимались в 03:00, база в 03:20, и за эти двадцать минут пользователи успели загрузить и удалить немало вложений.

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

Одно вложение, два дома

Возьмём типичную историю: пользователь прикрепляет файл к заказу. Что происходит внутри, зависит от приложения, но схема почти всегда одна из двух.

  1. Приложение сначала записывает файл на диск, получает путь или идентификатор, затем вставляет строку в таблицу вложений со ссылкой на этот файл.
  2. Приложение сначала создаёт строку в базе со статусом «загружается», затем пишет файл и после этого меняет статус.

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

Теперь посмотрим на две копии, сделанные в разное время, и на то, какие расхождения возможны.

Что видно после восстановления при разном порядке копирования
ПорядокЧто может оказаться в копииКак это выглядит для пользователя
Сначала база, потом файлыФайлы новее базы: есть файлы без строк в таблицеЛишние «осиротевшие» файлы; пользователь ничего не замечает, растёт занятое место
Сначала файлы, потом базаБаза новее файлов: есть строки без файловБитая ссылка, вложение в заказе есть, открыть его нельзя

Таблица показывает асимметрию: если расхождение неизбежно, его последствия бывают разными. Лишний файл безвреден, а ссылка в никуда ломает пользовательскую работу. Если вы по каким-то причинам не можете получить строго одновременный снимок, то порядок «сначала база, потом файлы» даёт более мягкий отказ, при условии что приложение не удаляет файлы сразу при удалении записи. Это наше рассуждение, а не правило для всех систем: если файлы удаляются, то картина усложняется, ведь файл мог исчезнуть из каталога, пока база в копии о нём ещё помнит. Поэтому прежде чем выбирать порядок, нужно знать, как ведёт себя ваше приложение при удалении и замене вложений.

Что вообще нужно сохранять

Compose-стек состоит из нескольких видов вещей, и относиться к ним нужно по-разному. Самая частая ошибка — думать, что файл docker-compose.yml и есть резервная копия. Он описывает, как запустить сервисы, но не содержит ни одной строки заказа и ни одного загруженного файла.

Документация Docker отмечает, что одну и ту же Compose-конфигурацию можно применять в CI, staging и production с различающимися настройками: в production может понадобиться убрать bind mount с кодом, изменить порты и переменные окружения, добавить политику перезапуска (Docker Compose: Production). Для резервного копирования отсюда следует практический вывод: сохранять нужно ту конфигурацию, с которой стек действительно работает на боевом сервере, включая переопределяющие файлы, а не только базовый файл из репозитория. Если на сервере запуск идёт с несколькими файлами конфигурации или с файлом окружения, вам нужно знать и их полный набор.

Удобно разложить содержимое стека на четыре корзины.

  • Состояние. Каталог данных базы, каталог загрузок, всё, что пользователи или приложение создали во время работы. Это невосстановимо из других источников, и именно на нём держится согласованность.
  • Конфигурация запуска. Все Compose-файлы, которые реально использовались, файл с переменными окружения, конфигурации прокси и приложения, подмонтированные снаружи. Без них вы поднимете что-то похожее, но не то же самое.
  • Секреты. Пароль базы, ключи подписи сессий, токены интеграций. Их нужно сохранять, иначе после восстановления зашифрованные или подписанные данные станут нечитаемыми, но хранить их следует отдельно от обычной копии данных и с отдельным контролем доступа. Копия, которая лежит рядом с файлами и открыта всем, делает утечку проще.
  • Воспроизводимое. Образы приложения и служебных сервисов. Они собираются из исходников или скачиваются по тегу, поэтому копировать их заново в каждую резервную копию обычно не нужно.

Про последнюю корзину стоит сказать аккуратно. Образ не содержит пользовательских данных автоматически, поэтому его отсутствие в копии не лишает вас информации. Но воспроизводимость зависит от того, что вы можете вновь получить тот же образ. Если вы используете плавающий тег вроде latest, через полгода вы получите другую версию, с которой старые данные могут оказаться несовместимы. Для восстановления полезно записать точные теги или дайджесты образов, которые работали на момент копии. Это строка в тексте, а не гигабайты данных, поэтому в копию её включить дёшево. Документация также напоминает, что обновление кода внутри образа требует пересборки образа и пересоздания контейнера (Docker Compose: Production), так что код в контейнере не является местом хранения данных, и менять его можно, не трогая тома.

Почему копия каталога базы не равна резервной копии

Теперь о самом соблазнительном и самом опасном пути. База лежит в именованном томе. Кажется, достаточно заархивировать содержимое тома, пока контейнер работает. Файлы скопируются, архив получится, команда завершится без ошибок.

Проблема в том, что работающая база непрерывно меняет свои файлы, и меняет их не в той последовательности, в какой их читает архиватор. Часть страниц данных уже записана, часть ещё ожидает в журнале, а архиватор читает файлы по очереди, в разные моменты. Результат может быть смесью состояний, которая не соответствует ни одному реальному моменту жизни базы. Поэтому копирование каталога работающего PostgreSQL как согласованной копии без специальной процедуры считать нельзя. Том для базы нужен для хранения данных между перезапусками контейнера, а не как формат резервной копии.

Для PostgreSQL документация описывает именно такую процедуру: непрерывное архивирование и восстановление на момент времени (PITR). Оно сочетает базовую файловую копию с архивом журнала предзаписи (WAL), и это позволяет воспроизвести изменения до выбранной точки (PostgreSQL: Continuous archiving and PITR). Ключевая идея: сама по себе файловая копия работающей базы может быть внутренне нестабильной, а журнал даёт возможность довести её до согласованного состояния при восстановлении.

Из этого вытекают две практические детали.

  • Базовая копия без нужного диапазона WAL бесполезна. Документация подчёркивает, что WAL необходим для восстановления и что одна базовая копия без требуемого диапазона не обеспечивает произвольный PITR. Значит, архив журнала нужно хранить вместе с базовой копией и проверять, что в нём нет пропусков.
  • Восстановление создаёт отдельную временную линию, и архивирование с возвратом требует планирования. Нельзя на ходу решить, что «ну, как-нибудь вернёмся»: нужно заранее понимать, куда пишутся архивы, сколько места они занимают и как выбирается точка восстановления.

Если у вас другая СУБД, то механизм будет другим. У MySQL, MongoDB или SQLite свои инструменты и свои гарантии, и универсального порядка шагов «для всех баз» не существует. Общий принцип один: использовать механизм, который сама база предлагает для согласованного снимка, а не обходиться копированием её файлов. Дальше в статье мы опираемся на PostgreSQL как на пример, потому что для него есть подтверждённая процедура, а для остальных вам нужно найти аналог в их документации.

Один момент времени для базы и файлов

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

Приложение только добавляет файлы

Если вложения неизменяемы: загрузили, больше не перезаписываем, не удаляем (или удаляем только отложенной уборкой), то задача проще. Файлы только появляются, и более новая копия файлов всегда содержит всё, что было в более старой. Тогда разумный порядок такой: сначала зафиксировать базу и запомнить момент, затем скопировать каталог. В копии каталога окажутся и все файлы, на которые ссылается база, и, возможно, несколько новых, ещё не отражённых в базе. Это те самые безвредные «сироты» из таблицы выше. Их можно позже найти сверкой и удалить.

Тут проявляется смысл PITR: вы можете выбрать точку восстановления базы так, чтобы она была не позже, чем окончание копирования файлов. Файлы, скопированные позже, чем выбранная точка, просто окажутся лишними, а не недостающими.

Приложение перезаписывает и удаляет файлы

Если файл можно заменить под тем же именем или удалить сразу при удалении записи, порядок уже не спасает. Пока вы копируете каталог, файл мог поменяться, и в копии окажется версия, не соответствующая строке в базе, например по размеру или контрольной сумме. Здесь есть два пути.

  • Короткая остановка записи. Перевести приложение в режим обслуживания или остановить сервис, который принимает загрузки, на время фиксации базы и снимка файлов. Это самый понятный способ, и если окно в несколько минут допустимо, он даёт действительно общий момент времени. Документация Compose отдельно упоминает, что отдельный сервис можно обновить без пересоздания зависимостей (Docker Compose: Production), поэтому из общих возможностей Compose понятно, что управлять сервисами по отдельности можно. Но остановите вы сервис приложения или только переведёте его в режим «только чтение», зависит от того, что поддерживает ваше приложение, и это нужно проверить на нём.
  • Снимок на уровне файловой системы или хранилища. Если каталог лежит на системе со снимками, можно быстро зафиксировать состояние файлов и затем спокойно копировать снимок. Вопрос, как эта возможность реализована у вас, зависит от вашего окружения, и из нашего набора источников подтвердить её устройство нельзя, поэтому рассматривайте её как вариант, который нужно проверить на своей системе.

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

Файлы лежат вне Compose-стека

Отдельный случай — когда вложения хранятся не в томе, а во внешнем объектном хранилище. Тогда копия тома с файлами вообще не нужна, а согласованность упирается в то, как это хранилище версионирует объекты. Эта ситуация выходит за рамки того, что можно проверить по нашим источникам, и здесь придётся смотреть документацию вашего хранилища. Но сам принцип сохраняется: база и хранилище должны быть сопоставлены по времени.

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

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

АРЕНДОВАТЬ СЕРВЕР

Метка согласованности: простой приём, который экономит часы

Какой бы порядок вы ни выбрали, полезно сделать так, чтобы каждая копия сама подсказывала, что в ней должно быть. Для этого на момент копирования сохраняют небольшой манифест. Он может быть обычным текстовым файлом рядом с копией.

  • Время начала и окончания копирования базы и файлов, и выбранная точка восстановления.
  • Количество строк в таблице вложений и количество файлов в каталоге.
  • Идентификатор последнего вложения и его контрольная сумма.
  • Точные теги или дайджесты образов и список Compose-файлов, использованных при запуске.

Манифест нужен не для красоты. Когда вы будете восстанавливать копию через полгода, он позволит ответить на вопрос «получилось ли то, что должно было получиться» без догадок. Если после восстановления в таблице вложений на сто строк больше, чем записано в манифесте, значит, вы восстановили базу на более позднюю точку, чем файлы, и сразу видно, где искать причину.

Собственно копирование хранится в репозитории резервных копий. Если вы ещё не выбирали инструмент, посмотрите разбор установки и настройки restic на VPS: такой инструмент подходит для файловой части копии и для хранения манифеста рядом с ней. Но как и любой инструмент, он копирует файлы, а согласованность базы по-прежнему определяется тем, как вы получили её снимок.

Восстановление в чистое окружение, а не поверх живого

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

Порядок действий примерно такой.

  1. Подготовьте отдельный хост или хотя бы отдельный проект Compose с другим именем и другими портами, чтобы он не пересекался с боевым. Если проект восстанавливается на том же сервере, имена томов и контейнеров не должны совпадать с рабочими. Для разовой проверки подойдёт небольшой VPS: на сервере с root-доступом можно поставить Docker, развернуть стек и после проверки удалить. Подойдёт ли тариф, зависит от размера ваших данных и нагрузки приложения. Ресурсы тарифов, например 2 ГБ памяти и 40 ГБ диска у Ba или 4 ГБ и 80 ГБ у Uab, не являются обещанием ёмкости, поэтому ориентируйтесь на размер своей копии плюс запас на развёрнутую базу и журнал, и на то, как приложение себя ведёт под вашей нагрузкой.
  2. Верните конфигурацию запуска. Compose-файлы, файл окружения, внешние конфигурации. Здесь пригодится принцип из документации: настройки для разных сред различаются, и в тестовом окружении вы сознательно меняете порты, окружение и режим перезапуска (Docker Compose: Production). Но менять нужно только то, что относится к среде, а не то, что влияет на формат данных.
  3. Верните секреты из их отдельного хранилища, а не из той же копии, где лежат данные.
  4. Восстановите базу процедурой, соответствующей тому, как делалась копия. Для PostgreSQL с PITR это базовая копия плюс нужный диапазон WAL и выбранная точка. Документация предупреждает, что восстановление порождает отдельную временную линию (PostgreSQL: Continuous archiving and PITR), так что в тестовом окружении не подключайте архив так, чтобы он продолжал писать в ту же область, что и боевая система.
  5. Верните файлы в том приложения и проверьте права: владелец и режим доступа должны подходить пользователю, от имени которого работает процесс в контейнере. Это частый источник странных ошибок: файл на диске есть, а приложение его прочитать не может.
  6. Запустите стек и дождитесь готовности сервисов, прежде чем проверять результат.

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

Проверка по завершённой операции

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

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

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

Хорошо добавить и операцию в обратную сторону: загрузите новое вложение в восстановленное приложение и откройте его. Это покажет, что приложение не только читает старые данные, но и корректно пишет новые: последовательности идентификаторов продолжаются без конфликтов, каталог доступен для записи.

Как часто и сколько хранить

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

Пара практических соображений, вытекающих из сказанного выше.

  • Храните копию не только на том же сервере и не только на том же диске. Копия на одном носителе с источником не переживёт его потерю. Разные VPS у одного провайдера сами по себе не гарантируют, что они находятся на разном оборудовании, поэтому, если вам нужна независимость, выбирайте хранилище осознанно и проверяйте это условие отдельно.
  • Автоматические резервные копии не стоит считать частью любого тарифа, если это явно не указано в условиях. Планируйте свои собственные копии и проверяйте, куда они реально попадают.
  • Тренируйте восстановление по расписанию, а не только после аварии. Копия, которую ни разу не разворачивали, остаётся гипотезой.

Когда вложение открывается у пользователя

Вернёмся к заказу с тремя вложениями. Правильная копия выглядит скучно: манифест говорит, что в таблице вложений 18 442 строки, а в каталоге столько же файлов плюс два свежих «сироты», появившихся уже после точки фиксации базы. Образы записаны по точным тегам, секреты лежат отдельно, Compose-файлы те же, что на боевом сервере. Числа здесь условные и приведены только для примера.

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