В 02:00 cron запускает pg_dump, сжимает дамп и отправляет его в S3. Утром в бакете лежит postgres_2026-07-20_02-00-01.sql.gz. Этот объект подтверждает загрузку, но не доказывает, что из него получится восстановить PostgreSQL.
Тебе нужен не архив по расписанию, а зафиксированный предел потери данных и проверенная процедура восстановления. Сначала выбери RPO, затем настрой выгрузку и разверни копию отдельно от продакшена.
Выбери стратегию по допустимой потере данных
Ежедневный pg_dump подходит, если при аварии можно потерять изменения за последние 24 часа. В схеме четыре компонента: pg_dump, gzip, AWS CLI и cron. Этого хватает для большинства PostgreSQL на VPS (VPS.DO, TechResolve).

Если нельзя терять больше часа, одного ночного дампа мало. Включай WAL-архивирование: PostgreSQL будет сохранять сегменты журнала транзакций, чтобы восстановить состояние между полными копиями. RPO сократится до минут на один архивированный WAL-сегмент (VPS.DO).
Для базы крупнее 50 ГБ источник рекомендует pgBackRest. Он делает полные, дифференциальные и инкрементальные копии и пишет их в S3-совместимое хранилище (VPS.DO).
Получается такая развилка:
| Требование | Стратегия |
|---|---|
| Допустима потеря данных до 24 часов | Ежедневный pg_dump |
| Нельзя потерять больше часа | Полная копия плюс WAL-архив |
| База больше 50 ГБ, нужны инкременты | pgBackRest |
Порог в 50 ГБ — ориентир, а не ограничение PostgreSQL. Замерь длительность дампа и восстановления на своём диске: база на NVMe и база того же размера на загруженном сетевом томе покажут разные результаты.
Собери бэкап из отдельных проверяемых этапов
Не сворачивай бэкап в pg_dump | gzip | aws s3 cp -... без обработки ошибок. Сохрани дамп локально и проверь код возврата каждого этапа. Такой подход соответствует схеме с отдельными проверками после pg_dump, gzip и aws s3 cp, которую приводит TechResolve.

#!/usr/bin/env bash
set -Eeuo pipefail
PATH=/usr/local/bin:/usr/bin:/bin
DB_NAME="app"
DB_HOST="127.0.0.1"
DB_USER="backup"
BACKUP_DIR="/var/backups/postgresql"
S3_URI="s3://company-postgres-backups/app"
RETENTION_DAYS=7
TIMESTAMP="$(date +%Y-%m-%d_%H-%M-%S)"
SQL_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql"
GZ_FILE="${SQL_FILE}.gz"
mkdir -p "$BACKUP_DIR"
pg_dump \
--host="$DB_HOST" \
--username="$DB_USER" \
--dbname="$DB_NAME" \
--file="$SQL_FILE"
gzip "$SQL_FILE"
aws s3 cp "$GZ_FILE" "${S3_URI}/$(basename "$GZ_FILE")"
find "$BACKUP_DIR" \
-type f \
-name "${DB_NAME}_*.sql.gz" \
-mtime "+${RETENTION_DAYS}" \
-delete
pg_dump экспортирует базу, gzip сжимает файл, aws s3 cp загружает архив в бакет. Метка date +%Y-%m-%d_%H-%M-%S создаёт отдельное имя для каждого запуска (TechResolve).
Скрипт включает строгую обработку ошибок Bash и выполняет очистку после команд создания, сжатия и загрузки дампа. Запусти его с тестовыми учётными данными и проверь код возврата каждого этапа, прежде чем добавлять задачу в cron.
Направь stdout и stderr cron-задачи в отдельный лог. Так ты увидишь ошибку авторизации AWS, нехватку места или недоступную команду:
0 2 * * * /opt/backup/postgres-to-s3.sh >> /var/log/postgres-backup.log 2>&1
Выражение 0 2 * * * запускает задачу каждый день в 02:00. Явный PATH внутри скрипта нужен из-за урезанного окружения cron: без него задача может не найти pg_dump или aws (TechResolve, VPS.DO).
Убери пароль из скрипта
Не записывай пароль в PGPASSWORD внутри файла. Положи его в .pgpass пользователя, от которого работает cron:
127.0.0.1:5432:app:backup:REPLACE_WITH_PASSWORD
Закрой файл от других пользователей:
chmod 600 ~/.pgpass
.pgpass позволяет избежать интерактивного ввода пароля. Для продакшена TechResolve рекомендует этот способ вместо PGPASSWORD, который легко оставить в скрипте или окружении процесса (TechResolve).
Проверь команду от того же Unix-пользователя, который запустит cron:
pg_dump \
--host=127.0.0.1 \
--username=backup \
--dbname=app \
--file=/tmp/app-check.sql
Если команда запрашивает пароль, cron не сможет выполнить бэкап без участия оператора.
Ограничь доступ к S3 одним бакетом
Создай отдельные AWS-учётные данные для бэкапа. Для загрузки объектов нужно как минимум действие s3:PutObject; ресурс можно ограничить префиксом конкретного бакета (TechResolve, CloudRay).

Минимальная политика для показанного скрипта выглядит так:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "UploadPostgresBackups",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::company-postgres-backups/app/*"
}
]
}
Выдавай только те действия, которые вызывает скрипт. Для показанной загрузки нужен s3:PutObject. Скрипту, который читает или удаляет удалённые архивы, потребуются отдельные разрешения s3:GetObject или s3:DeleteObject. В примерах CloudRay права на бакет и его объекты разделены (CloudRay).
После настройки проверь запись тестовым файлом:
aws s3 cp /etc/hostname \
s3://company-postgres-backups/app/access-check.txt
Успешная команда подтверждает право на запись. Качество дампа она не проверяет.
Настрой ротацию после успешной загрузки
Локальные архивы можно удалять через find -mtime. В примере срок хранения равен семи дням; такую схему ротации приводят TechResolve и VPS.DO.
Для S3 задай lifecycle policy на стороне бакета. Если объектами управляет сервер, rclone delete --min-age удаляет файлы старше заданного срока (VPS.DO). Учётной записи для этого понадобится s3:DeleteObject.
Не ставь одинаковый срок для локальной и удалённой копии без причины. Например, семь локальных архивов дают быстрый путь к недавней версии, а 30–90 удалённых копий сохраняют историю для поздно обнаруженной порчи данных. Срок определяют доступное место и требования к восстановлению.
Для RPO меньше часа архивируй WAL
WAL — журнал изменений PostgreSQL. Вместе с базовой копией его сегменты позволяют восстановить кластер до выбранной точки между полными бэкапами (VPS.DO).
Минимальные параметры в postgresql.conf:
wal_level = replica
archive_mode = on
archive_timeout = 300
archive_command = 'rclone copy %p s3:company-postgres-backups/wal/%f'
archive_mode = on включает архивирование. wal_level = replica задаёт подходящий уровень WAL. archive_timeout = 300 принудительно переключает сегмент примерно через пять минут, если PostgreSQL не заполнит его раньше (VPS.DO).
Этот интервал сам по себе не задаёт RPO в пять минут: архив должен успешно попасть в объектное хранилище. Следи за результатом archive_command и возрастом последнего загруженного WAL-сегмента.
Для восстановления нужны базовая копия и последовательность WAL после неё. Например, базу можно снять через pg_basebackup --format=tar --gzip, а сегменты получать через restore_command = 'rclone copy s3:company-postgres-backups/wal/%f %p' (VPS.DO).
Для инкрементальных копий используй pgBackRest
pgBackRest делает полные, дифференциальные и инкрементальные копии, поддерживает S3, LZ4 и шифрование AES-256-CBC (VPS.DO). Взамен придётся настроить stanza, репозиторий и отдельную процедуру восстановления.
После настройки проверь stanza:
pgbackrest --stanza=mydb check
Команда проверяет конфигурацию и доступность репозитория. Тестовое восстановление она не заменяет: pgBackRest позволяет развернуть копию в другом каталоге через --pg1-path, не затрагивая рабочий кластер (VPS.DO).
Выбирай pgBackRest ради инкрементов и управления репозиторием. Статус check подтверждает проверку конфигурации, а запущенная восстановленная база — пригодность копии.
Разверни копию отдельно от продакшена
Сначала проверь, что объект появился в бакете:
aws s3 ls s3://company-postgres-backups/app/
aws s3 ls показывает содержимое бакета и подтверждает факт выгрузки (CloudRay). Затем скачай последний архив на тестовый сервер:
aws s3 cp \
s3://company-postgres-backups/app/app_2026-07-20_02-00-01.sql.gz \
/tmp/app-restore.sql.gz
Создай пустую тестовую базу и восстанови дамп:
createdb --host=127.0.0.1 --username=postgres app_restore
gunzip -c /tmp/app-restore.sql.gz |
psql \
--host=127.0.0.1 \
--username=postgres \
--dbname=app_restore \
--set=ON_ERROR_STOP=on
Проверка через gunzip -c | psql показывает, что архив читается и PostgreSQL принимает его содержимое (VPS.DO). Параметр ON_ERROR_STOP добавлен, чтобы SQL-ошибка дала ненулевой результат проверки, а не затерялась среди последующих команд.
После импорта проверь данные, которыми пользуется приложение:
psql --dbname=app_restore --command="
SELECT count(*) AS users FROM users;
SELECT max(created_at) AS newest_order FROM orders;
"
Подставь инварианты своего проекта: число клиентов, дату последней транзакции, наличие расширений, последовательностей и функций. Код возврата 0 не обнаружит логически неполный дамп.
Бэкап можно считать рабочим после восстановления вне продакшена. До этого у тебя есть файл, но нет подтверждённого пути возврата сервиса.
Что проверить перед ближайшим запуском cron
- Зафиксируй RPO: до 24 часов — ежедневный
pg_dump; меньше часа — базовая копия и WAL; база больше 50 ГБ с требованием к инкрементам — pgBackRest (VPS.DO). - Проверь
.pgpassиз-под пользователя cron и закрой файл от остальных пользователей. - Оставь IAM-учётной записи доступ к нужному префиксу и только к тем операциям S3, которые выполняет скрипт.
- Запусти скрипт с тем же
PATHи от того же пользователя, что указаны в cron; сохрани stdout и stderr в лог (TechResolve). - Скачай последнюю копию и восстанови её в отдельную базу. Повторяй тест по расписанию, а не после аварии (VPS.DO).