Резервное копирование и восстановление

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

БазаЧто в нейКонтейнер
PostgreSQLАккаунты, организации, проекты, участники, правила алертов, каналы доставки, инциденты, настройки — всё, что вы настраивали руками в интерфейсе.postgres
ClickHouseСами события об ошибках, спаны трейсов, точки метрик, сэмплы профилей, результаты аптайм-проверок — весь объём телеметрии, которую прислали ваши приложения.clickhouse

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

Все команды ниже выполняются из папки репозитория (gotcha/, там же, где docker-compose.yml) и используют docker compose exec — то есть выполняют команду внутри уже запущенного контейнера, без необходимости пробрасывать порты баз наружу (они и не проброшены — см. Установку).

Backup: PostgreSQL

pg_dump — стандартная утилита логического бэкапа PostgreSQL, безопасно снимает копию с работающей базы без остановки сервиса:

mkdir -p backup
docker compose exec -T postgres pg_dump -U gotcha -d gotcha \
  | gzip > backup/postgres-$(date +%F).sql.gz

Разбор команды: docker compose exec -T postgres — выполнить внутри контейнера postgres (-T отключает псевдо-терминал, нужно при перенаправлении вывода в файл); pg_dump -U gotcha -d gotcha — выгрузить базу gotcha от имени пользователя gotcha (это дефолтные учётные данные из docker-compose.yml; если вы их меняли — подставьте свои); результат уходит на стандартный вывод, который мы сжимаем gzip и сохраняем на диск хоста с датой в имени файла.

Проверить, что файл не пустой и похож на дамп:

zcat backup/postgres-$(date +%F).sql.gz | head -20

Вы должны увидеть строки вида -- PostgreSQL database dump и CREATE TABLE ....

Backup: ClickHouse

ClickHouse хранит несравнимо больший объём данных, чем PostgreSQL, поэтому для него используется другой подход: выгрузка каждой таблицы во встроенном бинарном формате Native (компактный и быстрый для последующего восстановления той же версией ClickHouse).

Сначала узнайте список таблиц базы gotcha:

docker compose exec -T clickhouse clickhouse-client \
  --user gotcha --password gotcha --database gotcha \
  --query "SHOW TABLES"

Обратите внимание: SHOW TABLES возвращает и материализованные представления (transactions_5m, web_vitals_5m). Их выгружать и восстанавливать НЕ нужно: они наполняются автоматически при вставке в исходные таблицы, и восстановление их содержимого рядом с восстановлением transactions удвоит агрегаты — «Производительность» покажет вдвое завышенный трафик. Список таблиц для выгрузки фиксирован и приведён ниже.

Выгрузите каждую из них:

mkdir -p backup/clickhouse
for t in events transactions spans metric_points profile_samples check_results logs; do
  docker compose exec -T clickhouse clickhouse-client \
    --user gotcha --password gotcha --database gotcha \
    --query "SELECT * FROM $t FORMAT Native" \
    > backup/clickhouse/$t-$(date +%F).native
done

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

Более простой и абсолютно надёжный вариант — снапшот файловой системы с остановкой сервисов. Он гарантированно консистентен и для PostgreSQL, и для ClickHouse одновременно, ценой короткого простоя (обычно секунды-десятки секунд):

docker compose stop gotcha postgres clickhouse
docker run --rm \
  -v gotcha_pgdata:/pgdata:ro \
  -v gotcha_chdata:/chdata:ro \
  -v "$(pwd)/backup:/backup" \
  alpine tar czf /backup/volumes-$(date +%F).tar.gz /pgdata /chdata
docker compose start gotcha postgres clickhouse

(имена томов gotcha_pgdata/gotcha_chdata — префикс gotcha_ берётся из имени папки проекта; проверьте точное имя командой docker volume ls | grep gotcha, если оно отличается). Этот вариант хорошо подходит для ночного cron-задания, когда короткая недоступность приложения не критична.

Выбирайте один из двух подходов (живая выгрузка pg_dump+clickhouse-client, либо снапшот томов с простоем) — оба валидны, важно делать это регулярно и проверять, что бэкап действительно восстанавливается (см. ниже).

Бэкапьте .env, а не только базы

GOTCHA_SECRET_KEY шифрует секреты at-rest: client secret SSO, токены Telegram-ботов и ключи подписи вебхуков. Он живёт только в вашем .env (или в блоке environment: compose-файла) и никогда не попадает в базу — поэтому дамп PostgreSQL и ClickHouse сам по себе не является полным бэкапом.

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

cp .env "$BACKUP_DIR/env-$(date +%F)"
chmod 600 "$BACKUP_DIR/env-$(date +%F)"

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

Restore: PostgreSQL

Восстанавливать нужно в пустую базу и до старта приложения. Приложение при старте само применяет миграции (GOTCHA_AUTO_MIGRATE_ENABLED=true по умолчанию) — то есть создаёт все таблицы прежде, чем откроет порт, — и дамп, накатанный поверх, встретит уже существующую схему.

Восстановление полной копии (обе базы) — это один сквозной порядок, а не две независимые процедуры. Дамп PostgreSQL несёт собственную схему (CREATE TABLE внутри самого дампа), а вот в ClickHouse Native-дамп — это только строки: схему для него создаёт миграциями сам Gotcha. Между восстановлением PostgreSQL и вставкой в ClickHouse обязателен промежуточный шаг — применение миграций без запуска приложения, иначе таблиц ClickHouse ещё не существует и вставлять некуда:

# 1. Поднять ТОЛЬКО базы, без приложения: иначе оно создаст схему раньше дампа.
docker compose up -d postgres clickhouse

# 2. Пересоздать базу PostgreSQL начисто.
docker compose exec -T postgres psql -U gotcha -d postgres \
  -c 'DROP DATABASE IF EXISTS gotcha' -c 'CREATE DATABASE gotcha'

# 3. Накатить дамп PostgreSQL, останавливаясь на первой же ошибке.
gunzip -c backup/postgres-2026-07-01.sql.gz \
  | docker compose exec -T postgres psql -v ON_ERROR_STOP=1 --single-transaction -U gotcha -d gotcha

# 4. Применить миграции без запуска приложения: это создаёт схему ClickHouse
#    (и, если нужно, доводит схему PostgreSQL до версии бинаря), но не
#    открывает порт и не поднимает фоновые обработчики — вставлять в CH
#    можно сразу после этой команды, не опасаясь гонки с живым приложением.
docker compose run --rm --no-deps gotcha --migrate-only

# 5. Восстановить ClickHouse — см. раздел «Restore: ClickHouse» ниже.

# 6. И только теперь поднять приложение целиком.
docker compose up -d

ON_ERROR_STOP=1 и --single-transaction здесь обязательны, а не для красоты. Без них psql печатает поток ошибок relation ... already exists и duplicate key, завершается с кодом 0 и выглядит как успешное восстановление. При этом COPY в таблицу, у которой в новой схеме появились колонки, не проходит вовсе — и отличить ожидаемый шум от настоящего провала по такому выводу невозможно. С этими двумя флагами первая же ошибка останавливает восстановление, а транзакция откатывается целиком: либо восстановилось всё, либо база осталась пустой и это видно.

Если приложение уже работает на этой базе — остановите его (docker compose stop gotcha) до шага 2. Восстановление под работающим приложением означает, что оно продолжает писать в ту же базу параллельно.

Restore: ClickHouse

Восстановление таблицы, выгруженной в формате Native, обратной командой:

cat backup/clickhouse/events-2026-07-01.native | \
  docker compose exec -T clickhouse clickhouse-client \
    --user gotcha --password gotcha --database gotcha \
    --query "INSERT INTO events FORMAT Native"

Повторите для каждой таблицы. Таблица должна существовать (её создаёт шаг 4 из раздела «Restore: PostgreSQL» выше, --migrate-only) и быть пустой, иначе данные добавятся к уже имеющимся, а не заменят их.

Восстанавливаете во второй раз, в уже наполненную базу? Материализованные представления (transactions_5m, web_vitals_5m) нужно очистить до вставки в исходные таблицы, а не после. Они наполняются самой вставкой в transactions — очистка, выполненная постфактум, стирает и то, что вставка только что туда добавила, и раздел «Производительность» останется пустым при формально успешном восстановлении:

docker compose exec -T clickhouse clickhouse-client \
  --user gotcha --password gotcha --database gotcha \
  --query "TRUNCATE TABLE transactions_5m"
docker compose exec -T clickhouse clickhouse-client \
  --user gotcha --password gotcha --database gotcha \
  --query "TRUNCATE TABLE web_vitals_5m"

и только потом — вставка Native-дампов по команде выше.

Restore из снапшота томов

Если использовался вариант с tar томов:

docker compose down
docker run --rm \
  -v gotcha_pgdata:/pgdata \
  -v gotcha_chdata:/chdata \
  -v "$(pwd)/backup:/backup" \
  alpine sh -c "rm -rf /pgdata/* /chdata/* && tar xzf /backup/volumes-2026-07-01.tar.gz -C /"
docker compose up -d

Это разрушительная операция — она стирает текущее содержимое томов перед распаковкой архива. Убедитесь, что архив тот, что нужен, прежде чем запускать.

После восстановления — проверка

curl -sf http://localhost:59080/readyz

Затем откройте интерфейс, зайдите под своим пользователем, откройте проект и проверьте, что видны и настройки (алерты, участники), и данные (события в разделе «Проблемы»).

Пример cron-задания

Ежедневный бэкап PostgreSQL + ClickHouse в 3:30 ночи, с хранением 14 последних копий:

crontab -e

добавьте строку:

30 3 * * * cd /path/to/gotcha && /path/to/gotcha/backup.sh >> /var/log/gotcha-backup.log 2>&1

где backup.sh — небольшой скрипт со всеми командами выгрузки выше плюс чистка старых файлов, например:

#!/usr/bin/env bash
set -euo pipefail
cd /path/to/gotcha
mkdir -p backup/clickhouse
day=$(date +%F)

# Чистка старого — в trap, и регистрируется он ДО первой команды, которая может
# упасть. Иначе при set -e чистка не выполнилась бы именно тогда, когда выгрузка
# упала: до строки в конце скрипта он просто не дошёл бы, и место кончилось бы
# ровно тогда, когда бэкапы и так не снимаются.
trap 'find backup -type f -name "*.tmp" -delete; find backup -type f -mtime +14 -delete' EXIT

# Пишем во временный файл и переименовываем только после успеха. Без этого
# перенаправление создаёт файл ДО того, как pg_dump успеет отработать: он упал
# — а в папке лежит пустой .sql.gz, неотличимый от настоящей резервной копии,
# пока она не понадобится.
docker compose exec -T postgres pg_dump -U gotcha -d gotcha \
  | gzip > backup/postgres-$day.sql.gz.tmp
mv backup/postgres-$day.sql.gz.tmp backup/postgres-$day.sql.gz

for t in events transactions spans metric_points profile_samples check_results logs; do
  docker compose exec -T clickhouse clickhouse-client \
    --user gotcha --password gotcha --database gotcha \
    --query "SELECT * FROM $t FORMAT Native" \
    > backup/clickhouse/$t-$day.native.tmp
  mv backup/clickhouse/$t-$day.native.tmp backup/clickhouse/$t-$day.native
done

Не забудьте сделать скрипт исполняемым (chmod +x backup.sh) и, что важно, копировать содержимое папки backup/ за пределы этого же сервера (другой диск, S3-совместимое хранилище, другой сервер) — локальная копия не спасёт при выходе из строя самого сервера.

Что дальше

  • Установка.
  • Обновление — резервную копию нужно снимать перед каждым обновлением.
  • Конфигурация — переменные GOTCHA_*_RETENTION_DAYS, влияющие на то, сколько данных вообще накапливается в ClickHouse.