Бэкап Postgres на S3: Selectel и Yandex Object Storage
База данных без бэкапа — это база, которую вот-вот потеряешь. Я делаю бэкапы Postgres на S3-совместимое хранилище уже несколько лет, и за это время восстанавливалась с них раза три-четыре. Расскажу свою рабочую схему: как делать pg_dump, как заливать в Selectel или Yandex Object Storage, как ротация и проверка.
Контекст: Postgres 16 на VPS, Ubuntu 24.04. Хранилище — Selectel S3 или Yandex Object Storage. Оба S3-совместимы, разница только в endpoint и ключах.
Что бэкаплю
- Логический дамп через
pg_dump. Удобный, переносимый, можно восстанавливать частично. Минус — медленный на больших базах. - WAL-архив для восстановления на любую точку времени (PITR). Использую только на ответственных кластерах.
- Файловая копия данных через
pg_basebackupдля быстрого восстановления крупного кластера.
Для большинства небольших проектов хватает первого пункта. Расскажу про него подробно, остальные — кратко.
pg_dump раз в день
Самая простая схема: ежедневно дампим всю базу, заливаем в S3. Скрипт /opt/scripts/pg-backup.sh:
#!/bin/bash
set -euo pipefail
DATE=$(date -u +%FT%H%M%SZ)
HOST=${PGHOST:-127.0.0.1}
PORT=${PGPORT:-5432}
USER=${PGUSER:-postgres}
DB=${PGDATABASE:-app}
BUCKET=my-backups
DIR=/tmp/pg-backup
mkdir -p $DIR
# Глобальные объекты (роли, права)
pg_dumpall --globals-only -h $HOST -p $PORT -U $USER > $DIR/globals-$DATE.sql
# Сама база, custom-формат
pg_dump -h $HOST -p $PORT -U $USER -F c -Z 6 -f $DIR/$DB-$DATE.dump $DB
# Чексумма
sha256sum $DIR/$DB-$DATE.dump > $DIR/$DB-$DATE.dump.sha256
# Заливка на S3
aws --endpoint-url https://s3.storage.selcloud.ru \
s3 cp $DIR/$DB-$DATE.dump s3://$BUCKET/postgres/daily/
aws --endpoint-url https://s3.storage.selcloud.ru \
s3 cp $DIR/$DB-$DATE.dump.sha256 s3://$BUCKET/postgres/daily/
aws --endpoint-url https://s3.storage.selcloud.ru \
s3 cp $DIR/globals-$DATE.sql s3://$BUCKET/postgres/daily/
# Локальная очистка (не держим больше двух последних)
find $DIR -name "*.dump" -mtime +2 -delete
find $DIR -name "*.sha256" -mtime +2 -delete
find $DIR -name "*.sql" -mtime +2 -delete
echo "Backup ${DB}-${DATE}.dump uploaded"Что важно:
-F c— custom-формат pg_dump, поддерживает параллельное восстановление и выборочный restore таблиц.-Z 6— gzip-сжатие на лету. Для текстовых данных даёт x3-5 уменьшение.- SHA256-чексумма заливается рядом. На восстановлении первое, что я проверяю.
pg_dumpall --globals-only— отдельный файл с ролями и пермишенами. Без него на чистом сервере не получится восстановить пользователей.
S3-credentials и aws CLI
Для Selectel и Yandex используется aws CLI с правильным endpoint. Кладу credentials в файл ~/.aws/credentials для root-юзера, который запускает скрипт:
[default]
aws_access_key_id = <ACCESS_KEY>
aws_secret_access_key = <SECRET_KEY>
region = ru-1В Selectel ключи генерятся в кабинете S3 → «Сервисный пользователь». В Yandex — через сервисный аккаунт с ролью storage.editor. Endpoint:
- Selectel:
https://s3.storage.selcloud.ru - Yandex:
https://storage.yandexcloud.net
cron
Запуск раз в сутки в 3 ночи через системный cron:
# /etc/cron.d/pg-backup
0 3 * * * postgres /opt/scripts/pg-backup.sh >> /var/log/pg-backup.log 2>&1Запускаю от пользователя postgres, чтобы не возиться с паролями к БД (peer-auth подхватит). Логи отдельным файлом, на ротацию ставлю logrotate.
На systemd-таймерах это выглядит чище. Юнит-сервис:
# /etc/systemd/system/pg-backup.service
[Unit]
Description=Postgres backup to S3
[Service]
Type=oneshot
User=postgres
ExecStart=/opt/scripts/pg-backup.sh# /etc/systemd/system/pg-backup.timer
[Unit]
Description=Daily Postgres backup
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.targetsudo systemctl enable --now pg-backup.timer
sudo systemctl list-timers pg-backup.timerPersistent=true — если сервер был выключен в плановое время, бэкап догонится при старте.
Ротация в S3
Не самой задаче скрипта удалять старые копии. Это делает Lifecycle policy на bucket:
<LifecycleConfiguration>
<Rule>
<ID>daily-30days</ID>
<Status>Enabled</Status>
<Filter><Prefix>postgres/daily/</Prefix></Filter>
<Expiration><Days>30</Days></Expiration>
</Rule>
<Rule>
<ID>weekly-90days</ID>
<Status>Enabled</Status>
<Filter><Prefix>postgres/weekly/</Prefix></Filter>
<Expiration><Days>90</Days></Expiration>
</Rule>
</LifecycleConfiguration>Применяется через aws CLI:
aws --endpoint-url https://s3.storage.selcloud.ru \
s3api put-bucket-lifecycle-configuration \
--bucket my-backups \
--lifecycle-configuration file://lifecycle.jsonЯ держу: последние 30 ежедневных, последние 90 еженедельных, последние 12 ежемесячных. Этого достаточно, чтобы откатиться к любой важной дате.
Шифрование
Дамп не должен лежать в S3 в открытом виде. Включаю серверное шифрование bucket-а (SSE-S3 встроено) и дополнительно шифрую сам файл перед загрузкой:
openssl enc -aes-256-gcm -salt \
-in $DIR/$DB-$DATE.dump \
-out $DIR/$DB-$DATE.dump.enc \
-pbkdf2 -pass file:/etc/pg-backup-keyКлюч /etc/pg-backup-key генерится один раз и хранится в защищённом виде (например, в менеджере секретов хостера). Если потерять — бэкапы превратятся в кашу. Я дублирую его в офлайн-копии.
Проверка восстановления
Самое важное и часто пропускаемое. Никогда не верь, что бэкап работает, пока не восстановил его. Я делаю это раз в неделю автоматически:
#!/bin/bash
set -euo pipefail
LATEST=$(aws --endpoint-url https://s3.storage.selcloud.ru \
s3 ls s3://my-backups/postgres/daily/ | grep '\.dump$' | sort | tail -1 | awk '{print $4}')
aws --endpoint-url https://s3.storage.selcloud.ru \
s3 cp s3://my-backups/postgres/daily/$LATEST /tmp/$LATEST
# Проверка чексуммы
aws --endpoint-url https://s3.storage.selcloud.ru \
s3 cp s3://my-backups/postgres/daily/${LATEST}.sha256 /tmp/${LATEST}.sha256
sha256sum -c /tmp/${LATEST}.sha256
# Восстановление в test-базу
sudo -u postgres dropdb --if-exists restore_test
sudo -u postgres createdb restore_test
sudo -u postgres pg_restore -d restore_test -j 4 /tmp/$LATEST
# Простая проверка: есть ли таблицы
rows=$(sudo -u postgres psql -d restore_test -tAc "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'")
echo "Restored database has $rows tables"
[ "$rows" -gt 0 ] || (echo "FAIL: empty database"; exit 1)
sudo -u postgres dropdb restore_test
rm /tmp/$LATEST /tmp/${LATEST}.sha256
echo "Backup verified: $LATEST"Если хоть раз падает — алерт, разбираюсь руками. Проверка восстановления выручала меня дважды: один раз сломалась network на сервере и pg_dump давал битый файл, другой раз неправильно настроили S3 endpoint.
WAL-архив для PITR
Если нужно «откатиться к 14:30 вчера» — обычного дампа мало. Помогает archive_mode + archive_command. В postgresql.conf:
wal_level = replica
archive_mode = on
archive_command = '/opt/scripts/wal-upload.sh %p %f'
restore_command = '/opt/scripts/wal-download.sh %f %p'Скрипт wal-upload.sh закидывает каждый WAL-файл в S3:
#!/bin/bash
set -e
SRC=$1
NAME=$2
aws --endpoint-url https://s3.storage.selcloud.ru \
s3 cp $SRC s3://my-backups/postgres/wal/$NAMEWAL-архив занимает существенно больше места, чем дампы. Полезно держать его 1–2 недели и применять только при сложном инциденте.
pgBackRest как альтернатива
Если хочется не возиться руками со скриптами, есть pgBackRest — мощный инструмент для бэкапа Postgres. Он умеет:
- Полные и инкрементальные бэкапы.
- Прямую загрузку в S3.
- PITR с тонким контролем.
- Параллелизм на чтении и записи.
На больших кластерах он удобнее моих собственных скриптов. На маленьких — overhead. Я для проектов 100+ ГБ беру pgBackRest, для всего остального хватает pg_dump.
Подводные камни
Свободное место на сервере
Перед тем как лить в S3, dump лежит на диске. На 50 ГБ базе нужно 50 ГБ свободно (ну или до 30, если хорошо сжимается). Если место кончится — pg_dump упадёт ровно посередине, оставив битый файл. Я мониторю свободное место как отдельную метрику.
Большие транзакции и lock
pg_dump берёт ACCESS SHARE lock, который не мешает обычной работе, но мешает ALTER TABLE. Если у вас миграция в момент бэкапа — она встанет в очередь. Я планирую ночной бэкап в окно, когда деплои не идут.
Бэкап с реплики
Чтобы не нагружать мастер, можно делать pg_dump с реплики. Это нормально, но имей в виду: реплика может слегка отставать, и бэкап будет слегка устаревшим. Различия обычно секунды, на бэкапах не критично.
Сетевые ошибки
aws CLI умеет retry, но при больших файлах временами всё-таки падает. Я добавляю --cli-read-timeout 0 и проверяю код возврата. На очень больших дампах режу через split и заливаю частями.
Чек-лист, который я повторяю каждый квартал
- Скрипт работает, последний бэкап сегодня.
- Lifecycle policy включена и работает (старые объекты удаляются).
- Verify-скрипт восстанавливает базу без ошибок.
- Чексумма последнего файла совпадает.
- Креды S3 не утекли и ещё валидны.
- Дисковое место под dump не близко к концу.
- Документация на восстановление актуальна (где какой ключ, кто запускает, как).
Бэкапы — занудство, но именно от них зависит, потеряешь ли ты данные после первого инцидента. На моих проектах эта схема работает годами без вмешательства, и я сплю спокойно. Главное — не настраивать «и забыть», а проверять, что восстановление действительно работает.