Две вещи, которыми занимаются в неподходящий момент. Логи ищут, когда уже что-то сломалось. Бэкапы проверяют, когда уже что-то потеряли.
Обе настраиваются за час и обе окупаются в первый же неприятный день.
Логи наружу
Стандартный вывод контейнера читается через 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. Ротация — четыре строки. Скрипт копирования — двадцать. Всё вместе час работы.
А вот проверка восстановлением час не занимает, она занимает вечер, и именно поэтому её откладывают. Отложить её можно ровно до первого раза, когда бэкап понадобится по-настоящему, и тогда выяснится всё сразу: и что он битый, и что файлов в нём нет, и что сорок минут никто не готов ждать.