SQLite хранит всю базу в одном файле, поэтому бэкап кажется делом одной команды cp. На работающей базе это не так: копия может оказаться без последних транзакций или вовсе не открыться. Ниже — способы снять согласованную копию живой базы, автоматизировать их на Linux и Windows, отправить копию в S3 и восстановить её без сюрпризов.
Почему cp опасен
У SQLite два режима журнала, и оба ломают наивное копирование по-своему.
WAL-режим (PRAGMA journal_mode=WAL). Рядом с app.db лежат app.db-wal и app.db-shm. Новые транзакции сначала пишутся в -wal, а в основной файл переносятся позже, при checkpoint. Если скопировать только app.db, вы потеряете всё, что ещё не прошло checkpoint, — иногда это часы работы. Если скопировать все три файла по очереди, между копированиями приложение успеет записать что-то ещё, и файлы окажутся из разных моментов времени.
Rollback-журнал (режим по умолчанию, DELETE). Во время записи SQLite меняет страницы прямо в основном файле, а старые версии страниц держит в app.db-journal. Копия, снятая посреди транзакции, содержит наполовину изменённые страницы. Без парного журнала такой файл может не пройти integrity_check.
Вывод: файл живой базы нельзя копировать средствами файловой системы. Копию должен делать сам SQLite — он знает, какие страницы актуальны, и держит read-транзакцию на время чтения. cp безопасен только когда все процессы, которые пишут в базу, остановлены.
Способ 1: .backup в консоли sqlite3
Команда .backup использует online backup API: копирует базу постранично, учитывая содержимое -wal.
sqlite3 -cmd ".timeout 10000" /srv/app/data/app.db ".backup '/var/backups/sqlite/app.db'"
.timeout 10000 — это busy timeout: если база заблокирована записью, sqlite3 подождёт до 10 секунд вместо мгновенной ошибки database is locked.
Нюанс: .backup копирует порциями страниц. Если между порциями другой процесс изменил базу, копирование начинается заново. На базе с постоянной интенсивной записью это может затянуться — тогда удобнее VACUUM INTO.
Способ 2: VACUUM INTO
Начиная с SQLite 3.27 можно выполнить:
sqlite3 -cmd ".timeout 10000" /srv/app/data/app.db "VACUUM INTO '/var/backups/sqlite/app-2026-09-08.db'"
Что важно знать:
- Копия снимается внутри одной read-транзакции, то есть это согласованный снимок на момент начала команды. В WAL-режиме писатели при этом не блокируются; в режиме rollback-журнала запись ждёт окончания копирования.
- Целевой файл не должен существовать (допускается пустой файл), иначе команда завершится ошибкой. Поэтому в имени удобно держать дату и время.
- Результат дефрагментирован: свободные страницы выброшены, файл часто меньше оригинала.
- Результат — один самодостаточный файл без
-walи-shm, его можно сразу сжимать и отправлять. - Проверьте версию:
sqlite3 --version. В старых дистрибутивах может стоять версия ниже 3.27.
Минус — VACUUM INTO перестраивает всю базу, поэтому на больших файлах нагружает диск и процессор сильнее, чем .backup. Запускайте его в период низкой нагрузки.
Способ 3: backup API из кода
Если в контейнере нет консольного sqlite3 или бэкап удобнее запускать из самого приложения, используйте API языка.
Python (модуль sqlite3 из стандартной библиотеки, Python 3.7+):
import sqlite3
from datetime import datetime
out = f"/var/backups/sqlite/app-{datetime.now():%Y-%m-%d-%H%M}.db"
src = sqlite3.connect("/srv/app/data/app.db", timeout=10)
dst = sqlite3.connect(out)
src.backup(dst) # pages=-1 по умолчанию: вся база за один шаг
dst.close()
src.close()
С pages=-1 копирование идёт за один шаг под одной read-блокировкой. Если передать, например, pages=1000, копирование пойдёт порциями, и изменения из других соединений будут перезапускать процесс — как у .backup.
Node.js с better-sqlite3 (ES-модуль):
import Database from 'better-sqlite3';
const db = new Database('/srv/app/data/app.db', { fileMustExist: true });
db.pragma('busy_timeout = 10000');
await db.backup(`/var/backups/sqlite/app-${Date.now()}.db`);
db.close();
db.backup() возвращает промис и не блокирует event loop на всё время копирования.
Про PRAGMA wal_checkpoint(TRUNCATE)
Частый совет: «сделайте checkpoint и копируйте cp». Это не работает как гарантия:
- checkpoint не может перенести страницы, которые нужны активным читателям. Команда вернёт
busy = 1в первой колонке результата, и-walостанется непустым; - даже после успешного checkpoint приложение может записать новую транзакцию за миллисекунды до или во время
cp.
Checkpoint полезен перед остановкой приложения или чтобы -wal не разрастался, но для бэкапа живой базы используйте .backup, VACUUM INTO или API.
Скрипт: копия, проверка, сжатие, S3, ротация
#!/usr/bin/env bash
set -euo pipefail
DB=/srv/app/data/app.db
DIR=/var/backups/sqlite
TS=$(date +%F-%H%M)
OUT="$DIR/app-$TS.db"
mkdir -p "$DIR"
sqlite3 -cmd ".timeout 10000" "$DB" "VACUUM INTO '$OUT'"
# Проверка копии до отправки
CHECK=$(sqlite3 "$OUT" "PRAGMA integrity_check;")
if [ "$CHECK" != "ok" ]; then
echo "integrity_check: $CHECK" >&2
exit 1
fi
gzip -9 "$OUT"
# Yandex Object Storage
aws s3 cp "$OUT.gz" "s3://my-backups/sqlite/app-$TS.db.gz" \
--endpoint-url https://storage.yandexcloud.net --region ru-central1
# Локальная ротация: храним 14 дней
find "$DIR" -name 'app-*.db.gz' -mtime +14 -delete
Для Selectel поменяйте эндпоинт и регион: --endpoint-url https://s3.ru-1.storage.selcloud.ru --region ru-1. Ключи доступа положите в ~/.aws/credentials пользователя, от которого запускается скрипт, а не в сам скрипт.
Вместо aws CLI можно взять rclone. Пример remote в ~/.config/rclone/rclone.conf:
[yos]
type = s3
provider = Other
endpoint = https://storage.yandexcloud.net
region = ru-central1
access_key_id = ...
secret_access_key = ...
rclone copy "$OUT.gz" yos:my-backups/sqlite/
Ротацию в бакете лучше отдать lifecycle-правилу, чтобы старые копии удалялись без участия скрипта. В Yandex Object Storage правило можно задать в консоли или через S3 API:
{
"Rules": [
{
"ID": "expire-sqlite",
"Status": "Enabled",
"Filter": { "Prefix": "sqlite/" },
"Expiration": { "Days": 30 }
}
]
}
aws s3api put-bucket-lifecycle-configuration --bucket my-backups \
--lifecycle-configuration file://lifecycle.json \
--endpoint-url https://storage.yandexcloud.net
Поддержку lifecycle у другого провайдера проверьте в его документации. Если в базе есть персональные данные, держите хотя бы одну копию вне сервера с приложением: бэкап на том же диске не спасёт от потери диска.
Расписание: cron, systemd timer, Windows
Cron, каждую ночь в 03:15:
15 3 * * * /usr/local/bin/sqlite-backup.sh >> /var/log/sqlite-backup.log 2>&1
systemd timer удобнее: логи попадают в journald, а Persistent=true запустит пропущенный бэкап, если сервер был выключен в момент срабатывания.
# /etc/systemd/system/sqlite-backup.service
[Unit]
Description=SQLite backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/sqlite-backup.sh
# /etc/systemd/system/sqlite-backup.timer
[Unit]
Description=Nightly SQLite backup
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now sqlite-backup.timer
systemctl list-timers sqlite-backup.timer
На Windows понадобится sqlite3.exe в PATH. Скрипт C:\scripts\sqlite-backup.ps1:
$ErrorActionPreference = 'Stop'
$db = 'C:\app\data\app.db'
$dir = 'D:\Backups\sqlite'
$ts = Get-Date -Format 'yyyy-MM-dd-HHmm'
$out = Join-Path $dir "app-$ts.db"
New-Item -ItemType Directory -Force $dir | Out-Null
& sqlite3.exe -cmd '.timeout 10000' $db "VACUUM INTO '$out'"
if ($LASTEXITCODE -ne 0) { throw 'VACUUM INTO failed' }
$check = (& sqlite3.exe $out 'PRAGMA integrity_check;') -join "`n"
if ($check -ne 'ok') { throw "integrity_check: $check" }
Compress-Archive -Path $out -DestinationPath "$out.zip"
Remove-Item $out
Get-ChildItem $dir -Filter 'app-*.db.zip' |
Where-Object LastWriteTime -lt (Get-Date).AddDays(-14) |
Remove-Item
Compress-Archive в Windows PowerShell 5.1 не справляется с файлами больше 2 ГБ — для крупных баз используйте 7-Zip. Регистрация задачи в планировщике:
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -ExecutionPolicy Bypass -File C:\scripts\sqlite-backup.ps1'
$trigger = New-ScheduledTaskTrigger -Daily -At 3:15am
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable
Register-ScheduledTask -TaskName 'SQLite backup' -Action $action -Trigger $trigger `
-Settings $settings -User 'NT AUTHORITY\SYSTEM' -RunLevel Highest
-StartWhenAvailable — аналог Persistent=true: пропущенный запуск выполнится, когда компьютер снова включится.
Проверка копии
PRAGMA integrity_check читает всю базу и проверяет структуру страниц, индексы и их соответствие таблицам. На ok-копии он печатает одну строку ok. PRAGMA quick_check быстрее, потому что не сверяет содержимое индексов с таблицами, — его разумно гонять на очень больших базах каждый день, а полную проверку делать реже.
Проверка целостности говорит, что файл открывается и структура цела, но не говорит, что в нём нужные данные. Тест восстановления, сравнение числа строк и мониторинг «бэкап не пришёл» описаны в статье как проверить, что бэкап рабочий.
Восстановление
Порядок, при котором ничего не смешается:
sudo systemctl stop app # 1. остановить всех писателей
cd /srv/app/data
gunzip -c /var/backups/sqlite/app-2026-09-08-0315.db.gz > app.db.restore
sqlite3 app.db.restore "PRAGMA integrity_check;" # 2. проверить копию: ok
mkdir -p ../broken && mv app.db app.db-wal app.db-shm ../broken/ 2>/dev/null || true # 3. убрать старое
mv app.db.restore app.db # 4. атомарная замена
sudo systemctl start app
Зачем убирать -wal и -shm: если рядом с восстановленным файлом останется журнал от прежней базы, SQLite при открытии может попытаться применить его к чужому файлу — а это прямой путь к повреждению. Старые файлы лучше перенести в сторону, а не удалять: вдруг понадобятся для разбора.
Временный файл кладите в тот же каталог, что и базу: mv в пределах одной файловой системы атомарен, а между разными дисками превращается в копирование. Подробнее о восстановлении PostgreSQL, MySQL и DuckDB — в статье как восстановить базу из бэкапа.
Частые ошибки
- Бэкап лежит на том же диске, что и база. Нужна копия вне сервера.
- Скрипт без
set -euo pipefail: ошибка в середине не останавливает отправку битого файла. - Копия базы из Docker-тома снимается
cpс хоста при работающем контейнере. Запускайтеsqlite3внутри контейнера или останавливайте его — подробнее в статье про бэкап базы в Docker и Coolify. - Никто не замечает, что cron перестал срабатывать после переезда сервера. Нужен внешний контроль свежести бэкапа.
- Потеря даже нескольких минут недопустима, а бэкап раз в сутки. Тогда смотрите на непрерывную репликацию — сравнение в статье Litestream vs cron-скрипт vs dbsend.
Для типичных SQLite-проектов есть отдельные разборы: бэкап PocketBase и бэкап базы Telegram-бота.
Как это сделать в dbsend
dbsend — сервис версионированных бэкапов, в том числе для SQLite. CLI сам снимает согласованную копию через VACUUM INTO, поэтому ему можно передать путь к живой базе:
npm install --global @dbsend/sdk
dbsend login --key sqv_pk_…
dbsend init --database <id> --engine sqlite
dbsend backup /srv/app/data/app.db -l nightly
Каждая загрузка становится версией vN, при загрузке файл проверяется PRAGMA quick_check, для каждой версии хранится SHA-256. Если файл не изменился с прошлой версии, новая не создаётся. Историю смотрит dbsend log, восстановление делает dbsend restore <snapshotId> /srv/app/data/app.db --yes: файл скачивается во временный, сверяется sha256, проходит integrity_check, старые -wal/-shm рядом с целью удаляются (только с --yes), затем выполняется атомарное переименование. Приложение перед этим всё равно нужно остановить.
На платных тарифах есть diff между любыми двумя версиями (схема, таблицы, число строк) и алерты в Telegram, email или webhook, если бэкап не пришёл по расписанию. Хранение — в S3 в России, с шифрованием AES-256-GCM. Расписание запуска остаётся вашим: cron или timer из примеров выше вызывает dbsend backup. Бесплатный тариф: 1 база, 3 версии, хранение 7 дней; остальное — на странице тарифов. Все команды — в документации CLI.
Частые вопросы
Можно ли делать бэкап SQLite, не останавливая приложение?
Да, если копию делает сам SQLite: .backup, VACUUM INTO или backup API из Python и Node. Они читают базу в согласованном состоянии и учитывают содержимое -wal. Останавливать приложение нужно только при восстановлении.
Что выбрать: .backup или VACUUM INTO?
VACUUM INTO даёт согласованный снимок за одну транзакцию и компактный файл без -wal, но сильнее нагружает диск. .backup дешевле по ресурсам, но на базе с постоянной записью может перезапускаться. Для небольших и средних баз обычно удобнее VACUUM INTO.
Нужно ли копировать файлы -wal и -shm?
Нет. При .backup и VACUUM INTO результат уже включает данные из -wal. Файл -shm — служебный индекс в разделяемой памяти, в бэкапе он не нужен.
Как часто делать бэкап SQLite?
Исходите из того, сколько данных вы готовы потерять. Раз в сутки подходит для большинства небольших проектов; если потеря часа работы критична, запускайте бэкап чаще или используйте непрерывную репликацию WAL.
Как проверить, что бэкап SQLite не битый?
Выполните sqlite3 backup.db "PRAGMA integrity_check;" — должен вернуться ok. Затем откройте копию и проверьте, что в ключевых таблицах есть свежие строки: целостность файла ещё не значит, что в нём нужные данные.



