Автоматическое резервное копирование MongoDB в Docker: от скрипта до админки

База данных — сердце любого веб-приложения. Потерять её данные означает потерять всё: статьи, пользователей, комментарии, историю. При этом удивительно, как часто разработчики откладывают настройку бэкапов "на потом". Я решил не ждать, пока случится непоправимое, и реализовал простую, но надёжную систему резервного копирования для своего Nuxt-блога с MongoDB в Docker.
В этой статье расскажу, как я это сделал: от bash-скрипта с автоматической ротацией до API для скачивания бэкапов через админку — с уведомлениями в Telegram о каждом успешном бэкапе.
Что мы реализуем
- Bash-скрипт для создания бэкапов через
mongodump - Автоматическую ротацию — храним только последние N копий
- Telegram-уведомления о каждом бэкапе
- API-эндпоинты для просмотра и скачивания бэкапов из админки
- Страницу в админке со статистикой и возможностью скачивания
- Cron-задачу для ежедневного автоматического запуска
Стек: Nuxt 4, Nuxt UI, MongoDB 7, Docker Compose, Bash, Nitro API
Часть 1: Архитектура решения
Прежде чем писать код, определим, как всё будет работать:
┌─────────────────────────────────────────────────────────────────┐
│ Сервер (Host) │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Cron │───▶│ Скрипт │───▶│ ./backups/ │ │
│ │ (3:00 AM) │ │ backup.sh │ │ blog_20250108.gz │ │
│ └─────────────┘ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │
│ ▼ │ │
│ ┌─────────────┐ │ │
│ │ Telegram │ │ │
│ │ (алерт) │ │ │
│ └─────────────┘ │ │
├─────────────────────────────────────────────────────────────────┤
│ Docker Compose │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ blog-app │◀───│ mongodump │◀───│ blog-mongo │ │
│ │ (Nuxt) │ │ │ │ (MongoDB 7) │ │
│ │ │ └─────────────┘ └─────────────────────┘ │
│ │ /api/admin │ │
│ │ /backups │◀──── volume: ./backups:/app/backups:ro │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Ключевые решения:
- mongodump выполняется внутри контейнера MongoDB — не нужно устанавливать mongo-tools на хост
- Бэкапы хранятся на хосте в папке
./backups— переживут пересоздание контейнеров - Nuxt-приложение видит бэкапы через read-only volume — может отдавать их через API
- Скрипт запускается на хосте через cron — не зависит от состояния контейнеров
Часть 2: Скрипт резервного копирования
Создадим bash-скрипт, который будет:
- Создавать сжатый архив базы данных
- Удалять старые бэкапы (старше N дней)
- Отправлять уведомление в Telegram
Структура скрипта
Создайте файл scripts/backup-mongo.sh:
Полный код backup-mongo.sh
#!/bin/bash
# MongoDB backup script for blog
# Run via cron: 0 3 * * * /path/to/scripts/backup-mongo.sh >> /var/log/mongo-backup.log 2>&1
set -e
# Configuration
BACKUP_DIR="${BACKUP_DIR:-./backups}"
KEEP_DAYS="${KEEP_DAYS:-7}"
CONTAINER_NAME="${CONTAINER_NAME:-blog-mongo}"
DB_NAME="${MONGO_DB:-blog}"
# Generate filename with timestamp
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
FILENAME="blog_${TIMESTAMP}.gz"
echo "[$(date)] Starting backup..."
# Create backup directory if not exists
mkdir -p "$BACKUP_DIR"
# Create backup using mongodump
docker exec "$CONTAINER_NAME" mongodump \
--db="$DB_NAME" \
--archive=/tmp/backup.gz \
--gzip \
--quiet
# Copy backup from container
docker cp "$CONTAINER_NAME:/tmp/backup.gz" "$BACKUP_DIR/$FILENAME"
# Clean up temp file in container
docker exec "$CONTAINER_NAME" rm -f /tmp/backup.gz
# Get file size
SIZE=$(du -h "$BACKUP_DIR/$FILENAME" | cut -f1)
echo "[$(date)] Backup created: $FILENAME ($SIZE)"
# Remove old backups
DELETED=$(find "$BACKUP_DIR" -name "blog_*.gz" -mtime +"$KEEP_DAYS" -delete -print | wc -l)
if [ "$DELETED" -gt 0 ]; then
echo "[$(date)] Deleted $DELETED old backup(s)"
fi
# Send Telegram notification if configured
if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
COUNT=$(find "$BACKUP_DIR" -name "blog_*.gz" | wc -l | tr -d ' ')
MESSAGE="✅ <b>Бэкап создан</b>%0A%0A📦 <b>Файл:</b> $FILENAME%0A💾 <b>Размер:</b> $SIZE%0A📁 <b>Всего копий:</b> $COUNT"
curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
-d "text=${MESSAGE}" \
-d "parse_mode=HTML" > /dev/null
echo "[$(date)] Telegram notification sent"
fi
echo "[$(date)] Backup completed successfully"
Разбор ключевых моментов
Почему set -e?
Команда set -e останавливает скрипт при первой ошибке. Если mongodump упадёт — мы не будем пытаться копировать несуществующий файл.
Почему архив создаётся в контейнере?
docker exec "$CONTAINER_NAME" mongodump \
--db="$DB_NAME" \
--archive=/tmp/backup.gz \
--gzip
Мы запускаем mongodump внутри контейнера blog-mongo, потому что:
- Не нужно устанавливать mongo-tools на хост
- Версия mongodump гарантированно совместима с версией MongoDB
- Дамп создаётся локально, без сетевого оверхеда
Ротация через find
find "$BACKUP_DIR" -name "blog_*.gz" -mtime +$KEEP_DAYS -delete -print
-mtime +7— файлы старше 7 дней-delete— удаляет найденные файлы-print— выводит имена удалённых файлов (для подсчёта)
Формат имени файла
blog_20250108_153042.gz — дата и время в имени позволяют:
- Сортировать файлы хронологически
- Сразу видеть, когда создан бэкап
- Избежать коллизий при нескольких бэкапах в день
Сделайте скрипт исполняемым
chmod +x scripts/backup-mongo.sh
Часть 3: Настройка Docker Compose
Чтобы Nuxt-приложение могло отдавать бэкапы через API, нужно примонтировать папку с бэкапами.
Изменения в docker-compose.prod.yml
Обновлённая конфигурация app сервиса
services:
app:
image: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA:-latest}
container_name: blog-app
restart: unless-stopped
environment:
- NUXT_MONGODB_URI=mongodb://mongo:27017
- NUXT_MONGO_DB_NAME=${MONGO_DB}
- NUXT_JWT_SECRET=${JWT_SECRET}
- NUXT_JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
- NUXT_PUBLIC_SITE_URL=${SITE_URL}
- NUXT_TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
- NUXT_TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
- HOST=0.0.0.0
- PORT=3000
- BACKUP_DIR=/app/backups
volumes:
- uploads_data:/app/.output/public/uploads
- ./backups:/app/backups:ro # <-- Добавлено: read-only доступ к бэкапам
# ... остальная конфигурация
Важные моменты:
:ro— read-only mount, приложение может только читать бэкапыBACKUP_DIR=/app/backups— переменная окружения для API- Папка
./backupsсоздаётся на хосте, не внутри контейнера
Часть 4: API для работы с бэкапами
Создадим два эндпоинта:
GET /api/admin/backups— список всех бэкаповGET /api/admin/backups/[filename]— скачивание конкретного файла
Эндпоинт списка бэкапов
Создайте файл server/api/admin/backups/index.get.ts:
Полный код index.get.ts
import { readdir, stat } from 'fs/promises'
import { join } from 'path'
import { requireAdmin } from '../../../utils/auth'
const BACKUP_DIR = process.env.BACKUP_DIR || '/app/backups'
export default defineEventHandler(async (event) => {
await requireAdmin(event)
try {
const files = await readdir(BACKUP_DIR)
const backups = await Promise.all(
files
.filter(name => name.startsWith('blog_') && name.endsWith('.gz'))
.map(async (name) => {
const filePath = join(BACKUP_DIR, name)
const stats = await stat(filePath)
return {
name,
size: formatSize(stats.size),
sizeBytes: stats.size,
date: stats.mtime.toISOString()
}
})
)
// Sort by date descending (newest first)
backups.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
return { backups }
} catch (error: any) {
if (error.code === 'ENOENT') {
return { backups: [] }
}
throw createError({
statusCode: 500,
message: 'Failed to read backups directory'
})
}
})
function formatSize(bytes: number): string {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB'
}
Что возвращает эндпоинт:
{
"backups": [
{
"name": "blog_20250108_153042.gz",
"size": "2.4 MB",
"sizeBytes": 2516582,
"date": "2025-01-08T15:30:42.000Z"
},
{
"name": "blog_20250107_030000.gz",
"size": "2.3 MB",
"sizeBytes": 2411724,
"date": "2025-01-07T03:00:00.000Z"
}
]
}
Эндпоинт скачивания бэкапа
Создайте файл server/api/admin/backups/[filename].get.ts:
Полный код [filename].get.ts
import { createReadStream } from 'fs'
import { stat } from 'fs/promises'
import { join, basename } from 'path'
import { requireAdmin } from '../../../utils/auth'
const BACKUP_DIR = process.env.BACKUP_DIR || '/app/backups'
export default defineEventHandler(async (event) => {
await requireAdmin(event)
const filename = getRouterParam(event, 'filename')
if (!filename) {
throw createError({
statusCode: 400,
message: 'Filename is required'
})
}
// Validate filename format to prevent directory traversal
const sanitizedName = basename(filename)
if (!sanitizedName.startsWith('blog_') || !sanitizedName.endsWith('.gz')) {
throw createError({
statusCode: 400,
message: 'Invalid backup filename'
})
}
const filePath = join(BACKUP_DIR, sanitizedName)
try {
const stats = await stat(filePath)
setHeader(event, 'Content-Type', 'application/gzip')
setHeader(event, 'Content-Disposition', `attachment; filename="${sanitizedName}"`)
setHeader(event, 'Content-Length', stats.size)
return sendStream(event, createReadStream(filePath))
} catch (error: any) {
if (error.code === 'ENOENT') {
throw createError({
statusCode: 404,
message: 'Backup file not found'
})
}
throw createError({
statusCode: 500,
message: 'Failed to read backup file'
})
}
})
Важно: защита от Directory Traversal
const sanitizedName = basename(filename)
if (!sanitizedName.startsWith('blog_') || !sanitizedName.endsWith('.gz')) {
throw createError({ statusCode: 400, message: 'Invalid backup filename' })
}
Без этой проверки злоумышленник мог бы запросить ../../etc/passwd и получить системные файлы. Функция basename() убирает путь, а проверка формата гарантирует, что мы отдаём только файлы бэкапов.
Часть 5: Страница бэкапов в админке
API — это хорошо, но гораздо удобнее иметь визуальный интерфейс. Создадим страницу в админке, которая покажет:
- Количество бэкапов и занимаемое место
- Время последнего бэкапа
- Список всех бэкапов с кнопками скачивания
- Инструкцию по восстановлению
Добавление пункта в навигацию
Сначала добавим пункт "Бэкапы" в боковое меню админки.
В файле layouts/admin.vue найдите массив sidebarItems и добавьте:
const sidebarItems = [
{ label: 'Dashboard', icon: 'i-heroicons-home', to: '/admin' },
{ label: 'Статьи', icon: 'i-heroicons-document-text', to: '/admin/articles' },
// ... другие пункты
{ label: 'Бэкапы', icon: 'i-heroicons-circle-stack', to: '/admin/backups' }
]
Страница бэкапов
Создайте файл pages/admin/backups.vue:
Полный код backups.vue
<script setup lang="ts">
definePageMeta({
layout: 'admin',
middleware: 'admin',
title: 'Бэкапы'
})
interface Backup {
name: string
size: string
sizeBytes: number
date: string
}
const { getAuthHeaders } = useAuth()
const toast = useToast()
const { data, pending, refresh } = await useFetch<{ backups: Backup[] }>('/api/admin/backups', {
headers: getAuthHeaders()
})
const downloadBackup = async (backup: Backup) => {
try {
const response = await $fetch(`/api/admin/backups/${backup.name}`, {
headers: getAuthHeaders(),
responseType: 'blob'
})
const url = window.URL.createObjectURL(response as Blob)
const link = document.createElement('a')
link.href = url
link.download = backup.name
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
toast.add({
title: 'Загрузка началась',
description: backup.name,
color: 'success'
})
} catch (err: any) {
toast.add({
title: 'Ошибка загрузки',
description: err.message,
color: 'error'
})
}
}
const formatDate = (dateStr: string) => {
const date = new Date(dateStr)
return date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const getRelativeTime = (dateStr: string) => {
const date = new Date(dateStr)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffDays = Math.floor(diffHours / 24)
if (diffHours < 1) return 'только что'
if (diffHours < 24) return `${diffHours} ч. назад`
if (diffDays === 1) return 'вчера'
return `${diffDays} дн. назад`
}
const totalSize = computed(() => {
if (!data.value?.backups?.length) return '0 B'
const total = data.value.backups.reduce((acc, b) => acc + b.sizeBytes, 0)
if (total < 1024) return total + ' B'
if (total < 1024 * 1024) return (total / 1024).toFixed(1) + ' KB'
if (total < 1024 * 1024 * 1024) return (total / (1024 * 1024)).toFixed(1) + ' MB'
return (total / (1024 * 1024 * 1024)).toFixed(1) + ' GB'
})
useSeoMeta({
title: 'Бэкапы - Админ-панель'
})
</script>
<template>
<div>
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
Резервные копии базы данных
</h1>
<UButton
icon="i-heroicons-arrow-path"
variant="ghost"
:loading="pending"
@click="refresh()"
>
Обновить
</UButton>
</div>
<!-- Stats -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<UCard>
<div class="flex items-center gap-3">
<div class="p-2 bg-primary-100 dark:bg-primary-900/30 rounded-lg">
<UIcon name="i-heroicons-circle-stack" class="w-6 h-6 text-primary-500" />
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Всего копий</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white">
{{ data?.backups?.length || 0 }}
</p>
</div>
</div>
</UCard>
<UCard>
<div class="flex items-center gap-3">
<div class="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
<UIcon name="i-heroicons-server" class="w-6 h-6 text-success-500" />
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Занято места</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white">
{{ totalSize }}
</p>
</div>
</div>
</UCard>
<UCard>
<div class="flex items-center gap-3">
<div class="p-2 bg-warning-100 dark:bg-warning-900/30 rounded-lg">
<UIcon name="i-heroicons-clock" class="w-6 h-6 text-warning-500" />
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Последний бэкап</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white">
{{ data?.backups?.[0] ? getRelativeTime(data.backups[0].date) : '—' }}
</p>
</div>
</div>
</UCard>
</div>
<!-- Backups list -->
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h2 class="font-semibold text-gray-900 dark:text-white">Список бэкапов</h2>
<UBadge color="neutral" variant="subtle">
Хранятся 7 дней
</UBadge>
</div>
</template>
<div v-if="pending" class="flex justify-center py-8">
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin text-gray-400" />
</div>
<div v-else-if="!data?.backups?.length" class="text-center py-12">
<UIcon name="i-heroicons-circle-stack" class="w-12 h-12 text-gray-300 mx-auto mb-4" />
<p class="text-gray-500 dark:text-gray-400 mb-2">Бэкапов пока нет</p>
<p class="text-sm text-gray-400 dark:text-gray-500">
Они появятся после первого запуска скрипта резервного копирования
</p>
</div>
<div v-else class="divide-y divide-gray-200 dark:divide-gray-700">
<div
v-for="backup in data.backups"
:key="backup.name"
class="flex items-center justify-between py-4 first:pt-0 last:pb-0"
>
<div class="flex items-center gap-4">
<div class="p-2 bg-gray-100 dark:bg-gray-700 rounded-lg">
<UIcon name="i-heroicons-document-arrow-down" class="w-5 h-5 text-gray-500" />
</div>
<div>
<p class="font-medium text-gray-900 dark:text-white">
{{ backup.name }}
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ formatDate(backup.date) }} · {{ backup.size }}
</p>
</div>
</div>
<UButton
icon="i-heroicons-arrow-down-tray"
color="primary"
variant="soft"
@click="downloadBackup(backup)"
>
Скачать
</UButton>
</div>
</div>
</UCard>
<!-- Info -->
<UCard class="mt-6">
<template #header>
<h2 class="font-semibold text-gray-900 dark:text-white">Восстановление из бэкапа</h2>
</template>
<div class="prose prose-sm dark:prose-invert max-w-none">
<p>Для восстановления базы данных из бэкапа:</p>
<ol>
<li>Скачайте нужный файл бэкапа</li>
<li>Скопируйте файл на сервер</li>
<li>Выполните команды:</li>
</ol>
<pre><code>docker cp backup.gz blog-mongo:/tmp/
docker exec blog-mongo mongorestore \
--db=blog \
--archive=/tmp/backup.gz \
--gzip \
--drop</code></pre>
</div>
</UCard>
</div>
</template>
Что показывает страница
Статистика в карточках:
- Общее количество бэкапов
- Занимаемое место на диске (суммарный размер всех файлов)
- Время последнего бэкапа в относительном формате ("2 ч. назад", "вчера")
Список бэкапов:
- Имя файла с датой и временем
- Размер файла
- Кнопка скачивания для каждого бэкапа
Инструкция по восстановлению:
- Краткие шаги прямо на странице — не нужно искать документацию в критический момент
Скачивание через Blob
const downloadBackup = async (backup: Backup) => {
const response = await $fetch(`/api/admin/backups/${backup.name}`, {
headers: getAuthHeaders(),
responseType: 'blob'
})
const url = window.URL.createObjectURL(response as Blob)
const link = document.createElement('a')
link.href = url
link.download = backup.name
link.click()
window.URL.revokeObjectURL(url)
}
Почему так сложно? Потому что эндпоинт защищён авторизацией. Простая ссылка <a href="/api/admin/backups/file.gz"> не сработает — браузер не отправит JWT-токен. Приходится:
- Делать fetch с заголовком Authorization
- Получать файл как Blob
- Создавать временную ссылку через
createObjectURL - Программно кликать по ней
Часть 6: Настройка Cron
Для автоматического ежедневного бэкапа настроим cron на сервере.
Редактирование crontab
crontab -e
Добавьте строку:
0 3 * * * cd /path/to/project && MONGO_DB=blog TELEGRAM_BOT_TOKEN=xxx TELEGRAM_CHAT_ID=xxx ./scripts/backup-mongo.sh >> /var/log/mongo-backup.log 2>&1
Разбор:
0 3 * * *— каждый день в 03:00cd /path/to/project— переход в папку проекта (важно для относительных путей)MONGO_DB=blog— имя базы данныхTELEGRAM_*— переменные для уведомлений>> /var/log/mongo-backup.log 2>&1— логирование stdout и stderr
Проверка работы cron
# Просмотр активных задач
crontab -l
# Просмотр логов cron (Ubuntu/Debian)
grep CRON /var/log/syslog
# Просмотр наших логов бэкапа
tail -f /var/log/mongo-backup.log
Часть 7: Восстановление из бэкапа
Бэкап бесполезен, если вы не умеете из него восстанавливаться. Вот пошаговая инструкция:
Восстановление на том же сервере
# 1. Скопировать бэкап в контейнер
docker cp ./backups/blog_20250108_030000.gz blog-mongo:/tmp/
# 2. Восстановить базу (с удалением существующих данных)
docker exec blog-mongo mongorestore \
--db=blog \
--archive=/tmp/blog_20250108_030000.gz \
--gzip \
--drop
# 3. Удалить временный файл
docker exec blog-mongo rm -f /tmp/blog_20250108_030000.gz
Флаг --drop удаляет существующие коллекции перед восстановлением. Без него данные будут объединены, что может привести к дубликатам.
Восстановление на другом сервере
# 1. Скачать бэкап через API или SCP
curl -H "Authorization: Bearer $TOKEN" \
https://yoursite.com/api/admin/backups/blog_20250108_030000.gz \
-o backup.gz
# Или через SCP
scp user@server:/path/to/backups/blog_20250108_030000.gz ./
# 2. Скопировать в контейнер и восстановить
docker cp backup.gz blog-mongo:/tmp/
docker exec blog-mongo mongorestore \
--db=blog \
--archive=/tmp/backup.gz \
--gzip \
--drop
Частичное восстановление (одна коллекция)
# Восстановить только коллекцию articles
docker exec blog-mongo mongorestore \
--db=blog \
--collection=articles \
--archive=/tmp/backup.gz \
--gzip \
--drop
Часть 8: Тестирование
Перед тем как доверить бэкапам свои данные, протестируйте систему.
Ручной запуск скрипта
cd /path/to/project
MONGO_DB=blog ./scripts/backup-mongo.sh
Ожидаемый вывод:
[Wed Jan 8 15:30:42 MSK 2025] Starting backup...
[Wed Jan 8 15:30:45 MSK 2025] Backup created: blog_20250108_153042.gz (2.4M)
[Wed Jan 8 15:30:45 MSK 2025] Telegram notification sent
[Wed Jan 8 15:30:45 MSK 2025] Backup completed successfully
Проверка API
# Список бэкапов
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:3000/api/admin/backups
# Скачивание
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:3000/api/admin/backups/blog_20250108_153042.gz \
-o test-backup.gz
# Проверка архива
gunzip -t test-backup.gz && echo "Archive is valid"
Тест восстановления
Никогда не тестируйте восстановление на production-базе!
# Создать тестовый контейнер MongoDB
docker run -d --name mongo-test -p 27018:27017 mongo:7
# Скопировать и восстановить
docker cp backup.gz mongo-test:/tmp/
docker exec mongo-test mongorestore \
--archive=/tmp/backup.gz \
--gzip
# Проверить данные
docker exec mongo-test mongosh --eval "db.getSiblingDB('blog').articles.countDocuments()"
# Удалить тестовый контейнер
docker rm -f mongo-test
Возможные улучшения
Текущая реализация покрывает базовые потребности, но её можно расширить:
- Облачное хранение — автоматическая загрузка бэкапов в S3, Backblaze B2 или Google Cloud Storage
- Уведомления об ошибках — отдельное сообщение в Telegram, если бэкап упал
- Мониторинг свежести — алерт, если последний бэкап старше 25 часов
- Шифрование — GPG-шифрование архивов перед сохранением
- Ручной бэкап из админки — кнопка "Создать бэкап сейчас"
Итоги
Мы реализовали полноценную систему резервного копирования:
| Компонент | Что делает |
|---|---|
scripts/backup-mongo.sh | Создаёт бэкап, ротирует старые, шлёт в Telegram |
docker-compose.prod.yml | Монтирует папку бэкапов в контейнер |
GET /api/admin/backups | Возвращает список бэкапов с размерами и датами |
GET /api/admin/backups/[file] | Отдаёт файл для скачивания |
pages/admin/backups.vue | UI для просмотра и скачивания бэкапов |
| Cron | Запускает скрипт ежедневно в 3:00 |
Время на реализацию: пару часов. Спокойствие: бесценно.
Теперь, если что-то пойдёт не так, у меня всегда есть свежая копия базы данных. Я получаю уведомление в Telegram после каждого бэкапа и могу в любой момент скачать нужную копию прямо из админки.
Комментарии
Войдите, чтобы оставить комментарий
Пока нет комментариев. Будьте первым!