У большинства небольших Telegram-ботов вся ценность — в одном файле bot.db: пользователи, подписки, балансы, состояния диалогов. Если VPS пропал или неудачная миграция испортила таблицу, без бэкапа бот начинает жизнь с нуля, а пользователи — с вопросов в поддержку. Ниже — как делать копию базы прямо из бота или снаружи по расписанию, куда её отправлять и как восстановиться. Про выбор базы для бота есть отдельная статья: база данных для Telegram-бота.
Почему нельзя просто скопировать bot.db
Бот пишет в базу постоянно: каждое /start, каждое нажатие кнопки, каждое обновление FSM. Если скопировать файл командой cp в момент записи, можно получить копию с незавершённой транзакцией. В режиме WAL ещё хуже: последние изменения лежат в bot.db-wal, и копия одного bot.db их просто не содержит.
Правильные способы — онлайн-бэкап SQLite (.backup в CLI или Connection.backup() в Python) и VACUUM INTO. Оба создают консистентную копию, пока бот работает. Подробно о механике — в статье как сделать бэкап SQLite.
Бэкап изнутри бота на Python
Модуль sqlite3 из стандартной библиотеки умеет делать онлайн-бэкап. Операция синхронная и на большой базе может занять секунды, поэтому в асинхронном боте её выносят в отдельный поток через asyncio.to_thread, чтобы не останавливать обработку апдейтов.
# backup.py
import gzip
import shutil
import sqlite3
from datetime import datetime
from pathlib import Path
DB_PATH = Path("data/bot.db")
BACKUP_DIR = Path("data/backups")
def make_backup() -> Path:
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
ts = datetime.now().strftime("%Y%m%d-%H%M")
out = BACKUP_DIR / f"bot-{ts}.db"
src = sqlite3.connect(DB_PATH, timeout=30)
dst = sqlite3.connect(out)
try:
src.backup(dst) # консистентная копия живой базы
finally:
dst.close()
src.close()
check = sqlite3.connect(out)
try:
if check.execute("PRAGMA quick_check").fetchone()[0] != "ok":
raise RuntimeError(f"quick_check failed for {out}")
finally:
check.close()
gz = out.parent / (out.name + ".gz")
with open(out, "rb") as f_in, gzip.open(gz, "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
out.unlink()
# локальная ротация: 14 дней
cutoff = datetime.now().timestamp() - 14 * 86400
for old in BACKUP_DIR.glob("bot-*.db.gz"):
if old.stat().st_mtime < cutoff:
old.unlink()
return gz
Если бот работает через aiosqlite, ничего менять не нужно: бэкап открывает своё отдельное соединение sqlite3 в потоке. Чтобы чтение для бэкапа не мешало записи бота, включите в базе режим WAL (PRAGMA journal_mode=WAL;) и задайте busy_timeout в соединении бота.
Расписание в aiogram 3 через APScheduler
Пример для APScheduler 3.x (в 4.x API другой):
import asyncio
import os
from aiogram import Bot, Dispatcher
from aiogram.types import FSInputFile
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from backup import make_backup
ADMIN_CHAT_ID = int(os.environ["ADMIN_CHAT_ID"])
TG_LIMIT_MB = 45 # с запасом до лимита Bot API
async def backup_job(bot: Bot) -> None:
try:
path = await asyncio.to_thread(make_backup)
except Exception as e:
await bot.send_message(ADMIN_CHAT_ID, f"Бэкап не удался: {e}")
return
size_mb = path.stat().st_size / 1024 / 1024
if size_mb < TG_LIMIT_MB:
await bot.send_document(
ADMIN_CHAT_ID, FSInputFile(path), caption=f"{path.name}, {size_mb:.1f} МБ"
)
else:
await bot.send_message(ADMIN_CHAT_ID, f"{path.name}: {size_mb:.0f} МБ, только в S3")
async def main() -> None:
bot = Bot(token=os.environ["BOT_TOKEN"])
dp = Dispatcher()
scheduler = AsyncIOScheduler(timezone="Europe/Moscow")
scheduler.add_job(backup_job, "cron", hour=3, minute=0, args=[bot])
scheduler.start()
await dp.start_polling(bot)
asyncio.run(main())
python-telegram-bot
В PTB есть встроенная JobQueue (ставится как pip install "python-telegram-bot[job-queue]"):
import asyncio
import datetime as dt
import os
from zoneinfo import ZoneInfo
from telegram.ext import Application, ContextTypes
from backup import make_backup
ADMIN_CHAT_ID = int(os.environ["ADMIN_CHAT_ID"])
async def backup_job(context: ContextTypes.DEFAULT_TYPE) -> None:
path = await asyncio.to_thread(make_backup)
await context.bot.send_document(chat_id=ADMIN_CHAT_ID, document=path)
app = Application.builder().token(os.environ["BOT_TOKEN"]).build()
app.job_queue.run_daily(backup_job, time=dt.time(3, 0, tzinfo=ZoneInfo("Europe/Moscow")))
app.run_polling()
Проверку размера перед отправкой добавьте так же, как в примере для aiogram.
Telegraf и better-sqlite3
Если бот на Node.js использует better-sqlite3, у соединения есть метод db.backup(path), который возвращает промис и делает онлайн-бэкап:
const file = `backups/bot-${Date.now()}.db`;
await db.backup(file);
await bot.telegram.sendDocument(process.env.ADMIN_CHAT_ID, { source: file });
Расписание можно повесить на node-cron внутри процесса или на системный таймер, как описано ниже.
Ограничения отправки бэкапа в Telegram
Отправить файл админу удобно, но у этого способа есть пределы.
- Лимит размера. Через стандартный Bot API бот может отправить файл до 50 МБ. Для больших баз нужен собственный сервер Bot API или другое хранилище.
- Персональные данные. В базе бота обычно лежат Telegram ID, имена, телефоны, история заказов. Файл в чате доступен всем участникам чата и всем устройствам, где открыт аккаунт админа. Отправляйте в личный чат или закрытую группу с минимумом участников, а лучше — зашифрованный архив.
- Telegram — не хранилище бэкапов. Нет ротации, нет проверки целостности, сообщение можно случайно удалить. Используйте отправку в чат как удобное уведомление и быструю копию, а основной экземпляр храните в S3.
Шифрование архива через age перед отправкой:
age -r "$AGE_RECIPIENT" -o bot-20260926.db.gz.age bot-20260926.db.gz
# расшифровка на своей машине:
age -d -i ~/.config/age/key.txt -o bot-20260926.db.gz bot-20260926.db.gz.age
Бэкап снаружи: cron или systemd timer на VPS
Если не хочется трогать код бота, делайте копию отдельным скриптом. Это надёжнее: бэкап не зависит от того, жив ли процесс бота.
#!/usr/bin/env bash
# /usr/local/bin/backup-bot.sh
set -euo pipefail
DB=/opt/mybot/data/bot.db
DIR=/var/backups/mybot
TS=$(date +%Y%m%d-%H%M)
OUT="$DIR/bot-$TS.db"
mkdir -p "$DIR"
sqlite3 "$DB" ".backup '$OUT'"
sqlite3 "$OUT" "PRAGMA quick_check;" | grep -qx ok
gzip "$OUT"
aws s3 cp "$OUT.gz" "s3://bot-backups/mybot/bot-$TS.db.gz" \
--endpoint-url https://storage.yandexcloud.net
find "$DIR" -name 'bot-*.db.gz' -mtime +14 -delete
Для Selectel используйте --endpoint-url https://s3.ru-1.storage.selcloud.ru. Удаление старых объектов в бакете настройте правилом жизненного цикла в панели провайдера.
# /etc/systemd/system/backup-bot.service
[Unit]
Description=Telegram bot SQLite backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-bot.sh
# /etc/systemd/system/backup-bot.timer
[Unit]
Description=Daily bot backup
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup-bot.timer
Если бот крутится на Windows-машине, то же самое делает планировщик задач:
$action = New-ScheduledTaskAction -Execute "C:\bot\.venv\Scripts\python.exe" -Argument '-c "import backup; backup.make_backup()"' -WorkingDirectory "C:\bot"
$trigger = New-ScheduledTaskTrigger -Daily -At 3am
Register-ScheduledTask -TaskName "BotBackup" -Action $action -Trigger $trigger
Бот в Docker
Главное правило: база должна лежать в volume или bind mount, а не в слое контейнера, иначе она исчезнет при пересоздании контейнера.
services:
bot:
build: .
restart: unless-stopped
volumes:
- ./data:/app/data
С bind mount скрипт на хосте работает с ./data/bot.db напрямую. Если в образе нет CLI sqlite3, сделайте копию изнутри контейнера средствами Python:
docker compose exec bot python -c "import backup; print(backup.make_backup())"
Подробнее про именованные volume и Coolify — в статье бэкап базы в Docker и Coolify.
Восстановление
sudo systemctl stop mybot # или: docker compose stop bot
mv /opt/mybot/data/bot.db /opt/mybot/data/bot.db.broken
rm -f /opt/mybot/data/bot.db-wal /opt/mybot/data/bot.db-shm
gunzip -c /var/backups/mybot/bot-20260926-0300.db.gz > /opt/mybot/data/bot.db
sqlite3 /opt/mybot/data/bot.db "PRAGMA integrity_check;"
sudo systemctl start mybot # или: docker compose start bot
Старые -wal и -shm удаляйте обязательно: они относятся к прежней базе. После запуска проверьте /start, пару ключевых сценариев и число пользователей. Не ждите аварии, чтобы проверить этот порядок: раз в месяц восстанавливайте последний бэкап на тестовой машине — как это организовать, описано в статье как проверить, что бэкап рабочий. Общая инструкция по разным базам — как восстановить базу из бэкапа.
Как это сделать в dbsend
Вместо скрипта с S3 и ротацией можно отправлять базу в dbsend. CLI сам делает консистентную копию через VACUUM INTO, так что .backup перед ним не нужен:
npm install --global @dbsend/sdk
dbsend login --key sqv_pk_…
dbsend backup /opt/mybot/data/bot.db -d "$DBSEND_DATABASE_ID" -e sqlite -l "daily"
dbsend log
# восстановление: остановите бота, затем
dbsend restore <snapshotId> /opt/mybot/data/bot.db --yes
Эту команду можно поставить в тот же systemd timer вместо backup-bot.sh. Что добавляет dbsend: история версий с метками и закреплением, схема и число строк в каждой версии, сравнение любых двух версий (например, «сколько пользователей пропало после релиза»), алерт в Telegram, на email или в webhook, если бэкап не пришёл по расписанию (сравнение и алерты — на платных тарифах). Данные хранятся в S3 в России в зашифрованном виде. dbsend restore проверяет sha256 и PRAGMA integrity_check и с --yes удаляет устаревшие -wal/-shm. Код бота dbsend не меняет и сам базу не забирает — загрузку запускаете вы. Подробнее: бэкапы SQLite, документация CLI.
Если нужна копия с потерей не больше нескольких секунд, посмотрите сравнение Litestream, cron и dbsend. Если бот хранит данные в PocketBase, пригодится инструкция бэкап PocketBase.
Частые вопросы
Как часто бэкапить базу бота?
Минимум раз в сутки и обязательно перед каждым деплоем с миграциями. Если в базе платежи или балансы, делайте копии чаще или добавьте непрерывную репликацию.
Не заблокирует ли бэкап работу бота?
backup() из примера выше (без параметра pages) копирует базу за один шаг, в одной читающей транзакции. В режиме WAL чтение не блокирует запись, и бот продолжит отвечать. В режиме rollback journal запись может подождать окончания копирования, поэтому задайте боту busy_timeout.
Можно ли хранить бэкапы только в Telegram?
Не стоит. Есть лимит 50 МБ на файл для ботов, нет ротации и проверки целостности, а файл с персональными данными оказывается на всех устройствах участников чата. Используйте Telegram как уведомление, а копии храните в S3 или сервисе бэкапов.
Где хранить ключи S3 и токен бота для скрипта?
В переменных окружения сервиса или в файле с правами 600, например через EnvironmentFile= в systemd-юните. Не храните их в репозитории и в самом скрипте.
Что делать, если база бота уже повреждена?
Остановите бота, сохраните повреждённый файл вместе с -wal и -shm и восстановитесь из последнего бэкапа, прошедшего integrity_check. Повреждённую копию можно попробовать спасти командой .recover в CLI sqlite3, но восстановление из бэкапа надёжнее.



