almdev Разработка с ИИ

Логи и бэкапы: что монтировать и что восстанавливать
Бэкап, который никто не восстанавливал, бэкапом не является.

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

Подкатегория: Инфраструктура

Чтение
5 мин
Технологии / версии
Docker · PostgreSQL · Логи · Бэкапы
26 сен 2026 · 5 мин · 10 просмотров · Архив
Проверка восстановления данных из резервной копии
Инфраструктура 09/2026

Две вещи, которыми занимаются в неподходящий момент. Логи ищут, когда уже что-то сломалось. Бэкапы проверяют, когда уже что-то потеряли.

Обе настраиваются за час и обе окупаются в первый же неприятный день.

Логи наружу

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

Если приложению нужны файловые логи, каталоги можно вынести на хост. Фрагмент compose.yaml для Laravel-приложения:

  nginx:
    volumes:
      - ./logs/nginx:/var/log/nginx

  webserver:
    volumes:
      - ./logs/php:/var/www/html/storage/logs

Раскладка по сервисам, а не одна свалка. Логи nginx отдельно, PHP отдельно, воркеры отдельными файлами внутри каталога PHP.

Каталоги лучше создать при развёртывании с владельцем и правами пользователя приложения. Если отсутствующий каталог bind mount создаёт Docker daemon, владельцем часто оказывается root; непривилегированный процесс тогда может не получить право записи. Это зависит от образа и способа запуска, поэтому проверять надо фактические UID и права, а не наличие файла-заглушки в репозитории.

Ротация

Вопрос, который каждый считает чужим.

Docker ротирует свой журнал сам, если настроить драйвер:

    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Без этих строк журнал контейнера растёт без ограничений. У меня один раз файл журнала занял 14 гигабайт на сервере разработки — контейнер писал предупреждение в цикле три недели.

Смонтированные логи Docker не ротирует, это забота системы или самого приложения. Для nginx работает системная ротация, для PHP — настройки логгера самого фреймворка. Фрагмент канала Laravel:

'daily' => [
    'driver' => 'daily',
    'path' => storage_path('logs/laravel.log'),
    'days' => 14,
],

Ежедневный файл с хранением две недели. Единственная настройка, которая точно не зависит ни от системы, ни от Docker.

Что писать в лог

Правило, к которому я пришёл: запись должна отвечать на вопрос «что случилось с этим объектом», а не «код дошёл сюда».

Бесполезно:

$this->logger->info('Начинаем обработку');
$this->logger->info('Обработка завершена');

Полезно:

$this->logger->info('Резюме разобрано', [
    'resume_id' => $resume->getId(),
    'pages' => $document->pageCount(),
    'duration_ms' => $timer->elapsed(),
    'parser' => $parser::class,
]);

Идентификатор объекта в контексте — обязательно. Без него запись невозможно связать с обращением пользователя, а именно с этого начинается любое расследование.

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

Уровни, по которым не настроены оповещения, бессмысленны. Если ошибки никто не читает, они ничем не отличаются от отладки.

Три объекта копирования

База. Очевидный и обычно единственный, который делают.

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

Конфигурация и секреты. Настройки развёртывания, ключ приложения, пароли и токены внешних сервисов. Потеря ключа означает, что зашифрованные им значения не расшифровать.

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

Дамп PostgreSQL из контейнера

docker compose exec -T db \
    pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc \
    > "backups/db-$(date +%F-%H%M).dump"

Формат -Fc — собственный сжатый формат PostgreSQL. Он обычно компактнее текстового, поддерживает параллельное восстановление и позволяет выбрать отдельные объекты без разбора SQL-файла.

Флаг -T отключает выделение псевдотерминала. В неинтерактивном скрипте он нужен, если Compose иначе пытается открыть TTY; в некоторых окружениях команда работает и без него.

Восстановление:

docker compose cp backups/db-2026-08-20-0300.dump db:/tmp/restore.dump
docker compose exec -T db \
    pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
    --clean --if-exists --exit-on-error -j 4 /tmp/restore.dump

Флаг --clean удаляет восстанавливаемые объекты перед созданием, --if-exists не даёт упасть, если их нет, а -j 4 включает четыре параллельных задания. Для параллельного режима архив передаётся как файл, не через стандартный ввод; число заданий выбирается по ресурсам сервера.

Скрипт снятия копии заодно убирает старые:

find backups -name 'db-*.dump' -mtime +14 -delete

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

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

Единственный способ узнать, что бэкап рабочий.

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

docker compose exec -T db dropdb -U "$POSTGRES_USER" --if-exists restore_test
docker compose exec -T db createdb -U "$POSTGRES_USER" restore_test
time (docker compose exec -T db pg_restore -U "$POSTGRES_USER" -d restore_test < backups/latest.dump)

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

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

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

Что не бэкапить

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

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

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

Итог

Логи наружу — две строки в compose. Ротация — четыре строки. Скрипт копирования — двадцать. Всё вместе час работы.

А вот проверка восстановлением час не занимает, она занимает вечер, и именно поэтому её откладывают. Отложить её можно ровно до первого раза, когда бэкап понадобится по-настоящему, и тогда выяснится всё сразу: и что он битый, и что файлов в нём нет, и что сорок минут никто не готов ждать.

#docker #postgresql #logi #bekapy