Как настроить автоматические бэкапы базы данных: от cron до готового сервиса
Потеря базы данных — это не вопрос «если», а вопрос «когда». Дисковый сбой, случайный DROP TABLE, ошибочная миграция от AI-ассистента, взлом — любой из этих сценариев может произойти в самый неудобный момент. Автоматические бэкапы, которые вы никогда не видите и никогда не настраиваете вручную — вот цель. В этой статье разберём все способы: от базового cron-скрипта до production-ready решений.
Почему ручные бэкапы не работают
Ручной бэкап — это бэкап, который не делается. Руки доходят, когда уже поздно. Реальная статистика: по данным Veeam 2025 Ransomware Trends Report, 75% организаций, потерявших данные без автоматических бэкапов, не смогли полностью восстановить их. Для стартапов и indie-разработчиков это часто означает конец проекта.
Правило 3-2-1 для надёжного бэкапа:
- 3 — три копии данных
- 2 — на двух разных типах носителей
- 1 — одна копия в другом географическом месте
Для большинства web-проектов в 2026 году это переводится в: локальный дамп на VPS + копия в S3-совместимом хранилище (Яндекс Object Storage, Selectel, AWS S3).
Сценарий 1: cron + pg_dump (PostgreSQL на VPS)
Самый классический подход. Подходит для PostgreSQL на Ubuntu/Debian-сервере без дополнительных зависимостей.
Базовый скрипт бэкапа
#!/bin/bash
# /opt/scripts/backup-postgres.sh
set -e
# Настройки
DB_NAME="myapp_production"
DB_USER="postgres"
BACKUP_DIR="/var/backups/postgresql"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.sql.gz"
RETENTION_DAYS=30
# Создаём директорию если нет
mkdir -p "$BACKUP_DIR"
# Создаём дамп с компрессией
pg_dump -U "$DB_USER" "$DB_NAME" | gzip > "$BACKUP_FILE"
echo "Бэкап создан: $BACKUP_FILE ($(du -sh $BACKUP_FILE | cut -f1))"
# Удаляем старые бэкапы
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
echo "Старые бэкапы (>$RETENTION_DAYS дней) удалены"
# Сделать исполняемым
chmod +x /opt/scripts/backup-postgres.sh
# Добавить в crontab (редактируем от имени postgres)
crontab -e -u postgres
# Добавляем строку: каждый день в 3:00 ночи
0 3 * * * /opt/scripts/backup-postgres.sh >> /var/log/pg-backup.log 2>&1
Расширенный вариант с отправкой в S3
#!/bin/bash
# Расширенный скрипт с загрузкой в Яндекс Object Storage / AWS S3
DB_NAME="${DB_NAME:-myapp}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
TMP_FILE="/tmp/backup_${TIMESTAMP}.sql.gz"
S3_BUCKET="${S3_BUCKET:-s3://my-backups}"
S3_KEY="postgresql/${DB_NAME}/${TIMESTAMP}.sql.gz"
# Дамп
pg_dump "$DATABASE_URL" | gzip > "$TMP_FILE"
# Загрузка в S3 (aws cli или s3cmd для Яндекс/Selectel)
aws s3 cp "$TMP_FILE" "${S3_BUCKET}/${S3_KEY}" \
--endpoint-url "${S3_ENDPOINT:-https://storage.yandexcloud.net}"
# Чистим временный файл
rm -f "$TMP_FILE"
echo "Бэкап загружен: ${S3_BUCKET}/${S3_KEY}"
Переменные окружения для Яндекс Object Storage:
export AWS_ACCESS_KEY_ID="ваш_access_key"
export AWS_SECRET_ACCESS_KEY="ваш_secret_key"
export S3_ENDPOINT="https://storage.yandexcloud.net"
export S3_BUCKET="s3://ваш-бакет"
Бэкап MySQL/MariaDB через cron
#!/bin/bash
# backup-mysql.sh
DB_HOST="${DB_HOST:-localhost}"
DB_USER="${DB_USER:-root}"
DB_PASS="${MYSQL_PASSWORD}"
DB_NAME="${DB_NAME:-myapp}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="/var/backups/mysql/${DB_NAME}_${TIMESTAMP}.sql.gz"
mkdir -p /var/backups/mysql
mysqldump \
--host="$DB_HOST" \
--user="$DB_USER" \
--password="$DB_PASS" \
--single-transaction \
--routines \
--triggers \
"$DB_NAME" | gzip > "$BACKUP_FILE"
echo "MySQL бэкап: $BACKUP_FILE"
Бэкап SQLite (PocketBase, локальные базы)
SQLite-бэкап сложнее, чем кажется: простое копирование файла во время записи даёт повреждённый бэкап. Правильный подход — использовать .backup команду SQLite или hot backup API.
#!/bin/bash
# backup-sqlite.sh
DB_FILE="${DB_FILE:-/opt/pocketbase/pb_data/data.db}"
BACKUP_DIR="/var/backups/sqlite"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/pocketbase_${TIMESTAMP}.db"
mkdir -p "$BACKUP_DIR"
# Атомарный бэкап через SQLite CLI (online backup)
sqlite3 "$DB_FILE" ".backup '$BACKUP_FILE'"
gzip "$BACKUP_FILE"
echo "SQLite бэкап: ${BACKUP_FILE}.gz"
# Удаляем старые (хранить 7 дней)
find "$BACKUP_DIR" -name "*.db.gz" -mtime +7 -delete
Сценарий 2: GitHub Actions — бэкап без сервера
GitHub Actions — отличный вариант для managed-баз данных (Supabase, Neon, Railway), где у вас есть connection string, но нет прямого доступа к серверу.
# .github/workflows/daily-backup.yml
name: Daily Database Backup
on:
schedule:
- cron: '0 2 * * *' # каждый день в 2:00 UTC
workflow_dispatch: # ручной запуск при необходимости
jobs:
backup:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Install PostgreSQL client
run: sudo apt-get install -y postgresql-client
- name: Create database dump
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
pg_dump "$DATABASE_URL" | gzip > backup_${TIMESTAMP}.sql.gz
echo "BACKUP_FILE=backup_${TIMESTAMP}.sql.gz" >> $GITHUB_ENV
- name: Upload to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }}
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
S3_BUCKET: ${{ secrets.S3_BUCKET }}
run: |
aws s3 cp "${{ env.BACKUP_FILE }}" \
"s3://${S3_BUCKET}/backups/${{ env.BACKUP_FILE }}" \
--endpoint-url "$S3_ENDPOINT"
- name: Notify on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: '{"text": "⚠️ Database backup failed!"}'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
GitHub Actions предоставляет 2,000 минут/месяц бесплатно. Ежедневный бэкап занимает ~5-10 минут — укладываетесь в лимит на много месяцев вперёд.
Сценарий 3: Docker Compose sidecar
Если приложение запущено в Docker, удобно добавить отдельный контейнер-сайдкар, который делает бэкапы по расписанию через supercronic (cron без рута).
# docker-compose.yml
services:
app:
image: my-app:latest
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
backup:
image: alpine:latest
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
- S3_BUCKET=${S3_BUCKET}
- AWS_ACCESS_KEY_ID=${S3_ACCESS_KEY}
- AWS_SECRET_ACCESS_KEY=${S3_SECRET_KEY}
- AWS_DEFAULT_REGION=ru-central1
- S3_ENDPOINT=https://storage.yandexcloud.net
volumes:
- ./scripts/backup.sh:/backup.sh:ro
entrypoint: |
sh -c "
apk add --no-cache postgresql-client aws-cli supercronic &&
echo '0 */6 * * * /backup.sh' | supercronic -
"
depends_on:
- db
restart: unless-stopped
volumes:
pgdata:
Сценарий 4: Railway / Render — cron job сервис
На Railway можно добавить отдельный Cron Job сервис в том же проекте — он запускается по расписанию и не потребляет ресурсы между запусками.
// backup-job/index.ts — Bun/Node.js скрипт
import { exec } from 'child_process'
import { promisify } from 'util'
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { createReadStream } from 'fs'
const execAsync = promisify(exec)
const s3 = new S3Client({
region: 'ru-central1',
endpoint: process.env.S3_ENDPOINT,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY!,
secretAccessKey: process.env.S3_SECRET_KEY!,
},
})
async function backup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const filename = `backup_${timestamp}.sql.gz`
const tmpPath = `/tmp/${filename}`
// Создаём дамп
await execAsync(`pg_dump ${process.env.DATABASE_URL} | gzip > ${tmpPath}`)
console.log(`Дамп создан: ${tmpPath}`)
// Загружаем в S3
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: `backups/${filename}`,
Body: createReadStream(tmpPath),
ContentType: 'application/gzip',
}))
console.log(`Загружен в S3: backups/${filename}`)
}
backup()
.then(() => {
console.log('Бэкап успешно завершён')
process.exit(0)
})
.catch((err) => {
console.error('Ошибка бэкапа:', err)
process.exit(1)
})
Сценарий 5: dbsend.ru — один инструмент для всего
Все описанные выше сценарии работают, но требуют настройки, поддержки и мониторинга. dbsend.ru — сервис автоматических бэкапов баз данных, который берёт всю операционную нагрузку на себя.
Что даёт dbsend.ru в сравнении с самописным решением:
| Функция | cron + pg_dump | dbsend.ru | |---|---|---| | Настройка | 30-60 минут | 5 минут | | Мониторинг выполнения | Вручную (логи) | Автоматически | | Уведомления о сбоях | Нужно настраивать | Встроено | | Версионирование | Вручную | Автоматически | | Восстановление | Скрипт вручную | В один клик | | Проверка целостности | Нет | Автоматически | | Дашборд | Нет | Есть | | Поддержка разных БД | По одной | PostgreSQL, MySQL, SQLite, MongoDB |
Настройка dbsend.ru
# 1. Установка CLI
curl -sSL https://dbsend.ru/install.sh | bash
# 2. Аутентификация
dbsend login
# 3. Добавление источника (PostgreSQL)
dbsend source add \
--name="production-db" \
--db=postgresql://user:pass@host:5432/myapp \
--schedule="0 */6 * * *" \
--keep=30
# 4. Добавление SQLite (PocketBase)
dbsend source add \
--name="pocketbase" \
--db=sqlite:///opt/pocketbase/pb_data/data.db \
--schedule="*/30 * * * *" \
--keep=100
# 5. Проверка
dbsend status
Интеграция через SDK (Node.js/Bun):
import { DbSend } from 'dbsend'
const dbsend = new DbSend({ apiKey: process.env.DBSEND_API_KEY })
// Разовый бэкап по требованию (перед деплоем)
await dbsend.backup('production-db', {
label: `pre-deploy-${process.env.GIT_SHA}`,
})
// Получить список последних бэкапов
const backups = await dbsend.listBackups('production-db', { limit: 10 })
console.log(backups)
Интеграция в CI/CD (перед деплоем)
# .github/workflows/deploy.yml
- name: Backup before deploy
run: |
npx dbsend backup production-db --label="pre-deploy-${{ github.sha }}"
env:
DBSEND_API_KEY: ${{ secrets.DBSEND_API_KEY }}
- name: Deploy
run: ./deploy.sh
- name: Rollback on failure
if: failure()
run: |
# Восстановить последний бэкап перед деплоем
npx dbsend restore production-db --label="pre-deploy-${{ github.sha }}"
Аудит бэкапов: чем отличается хороший бэкап от плохого
Настроить бэкап и проверить бэкап — разные вещи. Хорошая стратегия включает:
Регулярное восстановление: хотя бы раз в месяц разворачивайте бэкап в тестовую базу и проверяйте целостность данных. Повреждённый бэкап — то же самое, что его отсутствие.
Мониторинг: каждый запуск бэкапа должен логироваться. Если бэкап не прошёл — вы должны узнать об этом немедленно, а не при попытке восстановления.
Версионирование: храните несколько версий бэкапа. Если данные повреждены логически (неверная миграция, XSS-инъекция), повреждение может обнаружиться через дни. Нужно иметь возможность откатиться на неделю назад.
Изоляция хранилища: бэкап на том же сервере, что и БД — не бэкап. Сбой диска уничтожит оба.
Чеклист: производственный стандарт бэкапов
- [ ] Бэкап настроен и запускается автоматически по расписанию
- [ ] Бэкапы хранятся в отдельном географическом месте (S3, облако)
- [ ] Уведомления о сбоях настроены (email, Telegram, Slack)
- [ ] Политика хранения определена (сколько дней/версий)
- [ ] Тестовое восстановление проводится регулярно
- [ ] Бэкап перед каждым деплоем (CI/CD интеграция)
- [ ] Доступ к бэкапам ограничен (не публичные S3-бакеты)
- [ ] RTO (Recovery Time Objective) и RPO (Recovery Point Objective) определены
FAQ
Как часто делать бэкапы базы данных?
Зависит от RPO — допустимой потери данных. Для большинства web-приложений: каждые 6-24 часа. Для финансовых или медицинских систем: каждые 1-15 минут (WAL shipping / continuous archiving). Для разработки и MVP: ежедневно. Минимальный стандарт: бэкап перед каждым деплоем.
Чем отличается pg_dump от pg_basebackup?
pg_dump — логический дамп: создаёт SQL-файл с командами CREATE TABLE и INSERT. Портативный, читаемый, подходит для восстановления на другой версии PostgreSQL. pg_basebackup — физический бэкап: копирует бинарные файлы данных. Быстрее для больших баз, но нужна идентичная версия PostgreSQL при восстановлении. Для большинства проектов достаточно pg_dump.
Можно ли делать бэкап PostgreSQL без даунтайма?
Да. pg_dump с флагом --lock-waits-timeout или --no-lock снимает данные без блокировки записи (MVCC гарантирует согласованность). pg_basebackup с --checkpoint=fast также работает без даунтайма. Ваше приложение не должно останавливаться для создания бэкапа.
Сколько хранить бэкапы?
Рекомендуемая стратегия: ежечасные бэкапы — 24 часа, ежедневные — 30 дней, еженедельные — 3 месяца, ежемесячные — 1 год. Для compliance-требований (ФЗ-152, GDPR) может потребоваться хранение до 5-7 лет. Хранилища S3 с жизненным циклом объектов (lifecycle policy) автоматизируют удаление старых бэкапов.
Нужно ли шифровать бэкапы?
Обязательно для production-данных с персональной информацией. Минимум: серверное шифрование на уровне S3 (SSE-S3 или SSE-KMS) — включается одной галочкой. Максимум: клиентское шифрование (GPG) до загрузки в хранилище. dbsend.ru шифрует бэкапы автоматически.
Как восстановить PostgreSQL из pg_dump?
# Восстановление из .sql файла
psql -U postgres -d myapp < backup.sql
# Восстановление из .sql.gz
gunzip -c backup.sql.gz | psql -U postgres -d myapp
# Восстановление из custom format (-Fc)
pg_restore -U postgres -d myapp backup.dump
Что делать, если бэкап повреждён?
Проверить целостность файла: gzip -t backup.sql.gz. Попробовать восстановить предыдущую версию бэкапа. Если используете S3 с versioning — доступны все исторические версии. Это одна из причин хранить несколько версий и проводить регулярные тест-восстановления.