Слово «даунтайм» (от англ. down time — «время простоя») означает, что сайт недоступен пользователям. Хороший бэкап — тот, который снимается без даунтайма: сайт работает, пока вы делаете копию, и вы не боитесь, что копия вышла «кривой». А ещё важнее другое умение: восстановиться без даунтайма, то есть вернуть сайт из копии так, чтобы пользователи почти ничего не заметили. Разберём оба крыла — снятие копии на живой машине и откат к чистой.
Часть I. Что именно бэкапим и почему одной кнопки не хватает
WordPress на VPS хранит данные в трёх местах разной природы. Их нельзя снять одним «экспортировать сайт» — каждый требует своего инструмента.
| Кит | Где лежит | Что это | Чем снимаем |
|---|---|---|---|
| Загрузки пользователя | wp-content/uploads/ | картинки, вложения, PDF — то, что физически нельзя восстановить из ниоткуда | файловый архиватор (tar) или синхронизатор (rsync) |
| Код и конфиг | сам WordPress (ядро, темы, плагины) + wp-config.php | исполняемая часть и настройки подключения | тот же tar; ядро/плагины в крайнем случае скачиваются заново, а вот wp-config.php — нет |
| База | MariaDB (не файл, а живой движок) | тексты постов, пользователи, настройки, таксономии | mysqldump |
Главная ошибка — думать, что «сайт = папка public_html». В папке лежат файлы и код, а тексты статей живут внутри MariaDB, в файловой ФС их нет. Потеряли базу — потеряли весь контент, даже если папки целы. И наоборот: папку uploads можно потерять за секунду при неудачном rm, а дамп базы её не вернёт.
Почему копия рядом не считается
Правило 3-2-1 — это не ритуал, а страховка от трёх разных сценариев:
- 3 экземпляра — оригинал плюс две копии: один сбойный диск не стёр всё.
- 2 разных носителя — чтобы отказ одного типа железа (дисковый контроллер, SSD, сетевой диск) не забрал обе копии одновременно.
- 1 вне площадки (off-site) — копия на другой машине/в объектном хранилище: потому что пожар в датацентре, неудачная миграция и взломщик, удаливший «и сайт, и бэкапы рядом», — это один удар, от которого спасает только экземпляр не на этой машине.
Прямо в этой связке уже стоит ловушка, о которой предупреждали соседи по серии: держать дамп базы в /var/backups/mysql/ на том же диске, что база — значит потерять данные и копии разом, когда диск кончился или умер (Диагностика, MariaDB). Отсюда: локальная копия — быстрый шаг восстановления на той же машине, off-site — страховка на случай потери машины целиком.
Что дальше в статье
Разберём по одному киту: базу без блокировки, файлы согласованно, автоматизацию, проверку и откат. По каждому — команды, которые реально выполняются.
Часть II. База без лок-а на живой машине
mysqldump — это программа, которая читает всю базу и превращает её в текстовый SQL-файл (команды CREATE TABLE + INSERT). Проблема — как он это читает.
По умолчанию (или с --lock-tables) дамп ставит блокировку на таблицы, пока вычитывает их. Для небольшой базы это секунды «тишины», для живой WordPress с потоком читателей и пишущих плагинов — заметный даунтайм: сайт на время дампа не отдаёт страницы или не сохраняет.
Для движка InnoDB (по умолчанию в современном WordPress/MariaDB) есть правильный путь — флаг --single-transaction:
# bash — снимок живой InnoDB-базы без блокировки таблиц
mysqldump \
--single-transaction \
--quick \
--routines --triggers --events \
--set-gtid-purged=OFF \
-u wpuser -p wpdatabase \
> /var/backups/wp/wp-db-$(date +%F-%H%M).sql
Что делает каждая опция, по делу:
--single-transaction— перед чтением открывает одну транзакцию. Для InnoDB это даёт согласованный снимок: дамп видит состояние базы «как было на момент BEGIN», даже если в этот момент кто-то пишет новые посты. Сайт не блокируется. Это ядро правила «без даунтайма».--quick— не кидает каждую таблицу целиком в оперативку дампера, а вычитает строками. Спасает от нехватки RAM на больших базах.--routines --triggers --events— по умолчанию дамп пропускает хранимые процедуры, триггеры и события. Без них восстановление потеряет часть логики.--set-gtid-purged=OFF— не вписывает в дамп служебные GTID-команды, из-за которых импорт на другой сервер иногда падает. Уберите, если GTID у вас не включены и дамп чистый.
Честные границы --single-transaction
Флаг снимает блокировку, но не всемогущ:
- Только для InnoDB. Если у вас таблицы движка MyISAM (старые установки или агрессивные плагины),
--single-transactionих не защитит — такие таблицы дамп всё равно блокирует. Проверьте, что всё InnoDB:
-- sql — какие таблицы не InnoDB (вернут пустой список = всё чисто)
SELECT TABLE_NAME, ENGINE
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'wpdatabase' AND ENGINE <> 'InnoDB';
- Во время дампа нельзя менять схему. Запущенный прямо в окно снятия дампа
ALTER TABLE(тяжёлый плагин обновился и меняет структуру) может дать конфликт. Окно дампа у базы в несколько сотен мегабайт — это десятки секунд, но в этот интервал не стоит ставить обновления (Обновления без простоя). - Согласованность только внутри базы. Она не захватывает файлы. Следующая часть — как совместить.
Часть III. Файлы и база «в один момент», чтобы восстановление сошлось
Настоящий снимок WordPress = база плюс файлы, снятые примерно в один момент. Если дамп базы был в 03:00, а копия файлов в 03:40, то после восстановления вы получите посты из 03:00, но картинки, загруженные к 03:40, — рассинхрон обычно не катастрофа, но при жёстких сценариях (миграция) виден.
Идеальный механизм — секундная заморозка записи (FLUSH TABLES WITH READ LOCK) на время, пока снимаете и базу, и файлы. Но она встает в лок на всё время снимка файлов, что противоречит «без даунтайма». На одной машине с WordPress pragmатично используют другое свойство:
wp-content/uploadsв нормальном режиме дополняется (новая картинка = новый файл), а не переписывается. Значит копия файлов, снятая чуть раньше или позже дампа, почти всегда содержит ту же или меньшую версию реального состояния, и ни один существующий файл не «испортился» от порядка. Этого достаточно для отката без сюрпризов.
Практичный набор команд (снимаем базу, потом файлы; rsync вместо tar, чтобы можно было тянуть off-site инкрементально):
# bash — снимок базы (см. Часть II), затем файлов, с записью какой дамп соответствующий
STAMP=$(date +%F-%H%M)
DEST=/var/backups/wp/$STAMP
mkdir -p "$DEST"
mysqldump --single-transaction --quick --routines --triggers --events \
--set-gtid-purged=OFF -u wpuser -p wpdatabase > "$DEST/db.sql"
# код + конфиг одним архивом (база данных в него НЕ входит — она в db.sql)
tar -czf "$DEST/site-files.tar.gz" \
-C /var/www/example.test \
wp-config.php wp-content/themes wp-content/plugins
# загрузки отдельно (самое крупное, самое ценное) — rsync сохраняет структуру и права
rsync -a --delete /var/www/example.test/wp-content/uploads/ "$DEST/uploads/"
echo "$STAMP" > "$DEST/.manifest" # метка: какой снимок с какой БД в паре
Зачем разделять site-files (код, обычно мегабайты) и uploads (может быть гигабайты и растёт):
- код меняется редко и целиком — его удобно держать как снапшот;
uploadsменяется часто и понемногу —rsyncтянет только новые файлы, инкрементально off-site.
--delete в rsync синхронизирует удалённую копию с оригиналом (что было удалено в originals — удалилось и из копии). При бэкапе на off-site по расписанию это правильно; но если вы однажды случайно снесёте файл на оригинале, rsync --delete через сутки снесёт его и из «свежей» копии. Отсюда — несколько точек ротации с временными метками (Часть IV), а не одна синхронизируемая папка.
Куда гнать off-site
На одном VPS проще всего — второй, не связанный, сервер или объектное хранилище (S3-совместимое, у провайдеров часто есть дешёвый «холодный» бакет). rsync поверх SSH тянет инкрементально:
# bash — разослать снимок на off-site (другой хост или бакет)
rsync -a --partial /var/backups/wp/ backup@offsite.example.test:/backups/codestack-vps/
(В примере offsite.example.test и backup@ — условные адреса; боевые хосты и ключи подставляете свои. Ключ SSH для backup@ — без пароля, заведённый специально под бэкап, и без прав на запись в сам сайт.)
Часть IV. Автоматизация через systemd-таймер
Разовый бэкап не работает — работает бэкап по расписанию, который никто не забывает. Классика — cron (crontab -e, строка вида 0 3 * * * /usr/local/bin/wp-backup.sh). Но на systemd-машине чище связка .service + .timer:
.service— что запускать (ваш скрипт). Отдельного демона для бэкапа держать не надо..timer— когда запускать и что делать с пропущенными запусками.
Плюс против cron ровно два: логи уходят в общий journalctl (легко смотреть, не гадать, куда cron дел вывод), и опция Persistent=true догоняет запуски, пропущенные, когда машина была выключена (cron их просто молча теряет).
Скрипт бэкапа положим в /usr/local/bin/wp-backup.sh (это тот же набор команд из Части III, плюс ротация):
# bash — /usr/local/bin/wp-backup.sh (фрагмент: снимок + ротация)
#!/usr/bin/env bash
set -euo pipefail
KEEP_LOCAL=14 # сколько ночных снимков держать на этой машине
STAMP=$(date +%F-%H%M); DEST=/var/backups/wp/$STAMP
mkdir -p "$DEST"
mysqldump --single-transaction --quick --routines --triggers --events \
--set-gtid-purged=OFF -u wpuser -p wpdatabase > "$DEST/db.sql"
tar -czf "$DEST/site-files.tar.gz" -C /var/www/example.test wp-config.php wp-content/themes wp-content/plugins
rsync -a --delete /var/www/example.test/wp-content/uploads/ "$DEST/uploads/"
echo "$STAMP" > "$DEST/.manifest"
# off-site: тянем только этот снимок, инкрементально по содержимому
rsync -a --partial "$DEST" backup@offsite.example.test:/backups/codestack-vps/
# ротация локальных: оставляем самые свежие KEEP_LOCAL каталогов-снимков,
# имена — это временные метки, сортировка по ним = хронологическая
ls -1dt /var/backups/wp/*/ | tail -n +$((KEEP_LOCAL+1)) | xargs -r rm -rf
(Логика ротации — «список каталогов-меток по времени убывания, всё кроме первых N удаляем»; это тот же приём, что советует Диагностика для логов, применённый к снимкам.)
Пара unit-файлов:
; ini — /etc/systemd/system/wp-backup.service
[Unit]
Description=WordPress backup (files + DB)
After=network.target mariadb.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/wp-backup.sh
# не плодить мусора, лог в journald
[Install]
WantedBy=multi-user.target
; ini — /etc/systemd/system/wp-backup.timer
[Unit]
Description=Nightly WordPress backup
Requires=wp-backup.service
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
[Install]
WantedBy=timers.target
OnCalendar=*-*-* 03:15:00 — каждый день в 03:15. Читаемый формат: «год-месяц-день время». Ставим на 03:15, а не на «красивый» 03:00, потому что в :00 на shared инфраструктуре провайдеров одновременно стартует вся cron-масса; в :15 очередь короче. Persistent=true — догнать запуск, если машина была выключена в 03:15 (например, после ночного обновления ядра из Обновлений).
Включаем таймер (не сам сервис — он запускается по таймеру):
# bash
sudo systemctl daemon-reload
sudo systemctl enable --now wp-backup.timer
systemctl list-timers | grep wp-backup # видно, когда следующий запуск
journalctl -u wp-backup.service -n 50 # логи последнего выполнения
Про структуру After=mariadb.service и [Install]/WantedBy — они разбирались в systemd; здесь то же знание применяется к задаче бэкапа.
Быстрее настроить таймер/крон сгенерированно
Ручная настройка .timer+.service (или, наоборот, crontab-строки с расписанием) — механическая рутина, где чаще всего путают формат OnCalendar и выражение cron. Это ровно то, что генерирует готовый инструмент на сайте, и он делает обе ветки — и systemd-таймер, и cron-строку — из описания по-человечески.
Инструменты под задачу
Чтобы собрать расписание под своё время и интервал (не держать в голове, как пишется OnCalendar и как устроен пятиполяный cron), в codestack.space есть Cron-генератор. Вы описываете интервал словами («каждый день в 03:15», «каждые 6 часов», «по понедельникам в 2 ночи»), а он отдаёт корректную строку crontab или готовый systemd .timer с OnCalendar и проверкой синтаксиса — то, что в Части IV набивается руками.
Генератор снимает рутину формата расписания; само решение «как часто бэкапить и сколько хранить» остаётся за вами — см. чек-лист ниже.
Часть V. Проверка: бэкап без восстановления = слух
Единственная операция, которая доказывает что копия существует, — это восстановить из неё что-то работающее. Не «файл лежит и размер похожий», а именно поднять. Без этого теста вы узнаете о том, что дамп битый, только в момент аварии, и уже поздно.
Проверяем дамп базы
Поднимите временную базу и импортируйте в неё дамп:
# bash — импорт дампа в чистую временную базу для проверки
mysql -u root -p -e "CREATE DATABASE wp_restore_check CHARACTER SET utf8mb4;"
mysql -u root -p wp_restore_check < /var/backups/wp/2026-10-23-0315/db.sql
# проверка, что таблицы и строки на месте
mysql -u root -p wp_restore_check -e "SHOW TABLES; SELECT COUNT(*) FROM wp_posts;"
Если db.sql поднялся без ошибок, wp_posts имеет разумное число строк — дамп восстановимый. (Про то, что такое utf8mb4, wp_posts и как читать содержимое базы — MariaDB.)
Одно частое «внешне зелёное, а на деле битое»: дамп на 0 КБ (забыли права, упал пароль) или дамп, оборванный на середине из-за set -e/таймаута. Поэтому после дампа полезно держать строгий тест — файл не пуст и заканчивается корректно:
# bash — базовая валидность дампа
test -s "$DEST/db.sql" || { echo "пустой дамп" >&2; exit 1; }
tail -c 200 "$DEST/db.sql" | grep -q "Dump completed" || echo "возможно оборван"
Проверяем файлы
Распакованный site-files.tar.gz и uploads/ — просто что они читается и что структура та же:
# bash — быстрая проверка архива кода
tar -tzf "$DEST/site-files.tar.gz" | grep -E "wp-config.php|functions.php" | head
find "$DEST/uploads" -type f | wc -l # количество файлов загрузок (просто сличаем с оригиналом)
Квартальные учения по полному откату
Раз в квартал поднимите сайт из копии на отдельном хосте/виртуалке и попробуйте им поработать: открыть главную, админку, добавить пост. Это единственный способ убедиться, что связка «код + БД + uploads» восстанавливается целиком, а не только по частям. Прогон занесите в ваш аварийный список (Runbook «Авария») — чтобы проверка была не «когда вспомним», а по плану.
Часть VI. Восстановление без даунтайма (или с минутным)
Здесь «без даунтайма» имеет два разных смысла — разберём оба, потому что от них зависит техника.
Сценарий A: «слетела база/файл, чиним на той же машине»
Минимальное окно простоя, пока подменяете объект. Пример — скатиться на состояние базы из ночного дампа:
# bash — откат базы к ночному снимку на той же машине (короткое окно недоступности)
mysql -u root -p -e "DROP DATABASE wpdatabase; CREATE DATABASE wpdatabase CHARACTER SET utf8mb4;"
mysql -u root -p wpdatabase < /var/backups/wp/2026-10-23-0315/db.sql
На время импорта сайт реально недоступен (несколько секунд–минут). Чтобы это выглядело предсказуемо, а не «белый экран», включите на это время режим обслуживания WordPress (wp-cli maintenance-mode activate, или временно создать .maintenance с аккуратной страницей) — пользователи увидят «техработы», а не ошибку. После восстановления выключите режим. Файлы, которых нет в базе (то же uploads), при таком откате не трогаем.
Важное правило отката, из которого выросла эта статья целиком: менять один слой за раз и держать второй SSH-сеанс живым (Диагностика). Откатили базу — проверили, что сайт отвечает, потом трогали файлы.
Сценарий B: «нужно увести сайт на новый хост без простоя» (миграция/отказ площадки)
Здесь даунтайм сведём к нулю за счёт переключения DNS, а не рестарта сервиса. Порядок:
- Готовим новый хост заранее (nginx, PHP-FPM, MariaDB — см. Анатомия VPS, Nginx, PHP-FPM).
- Переливаем копию файлов + дамп базы на новый хост и проверяем его по IP (не доменом) — сайт поднимается и работает из копии.
- Снижаем TTL (time-to-live — «сколько жить записи») у DNS-записи заранее (за сутки до), чтобы переключение стало быстрым: пока TTL короткий, старые адреса кэшируются в браузерах недолго и трафик уходит на новый хост почти сразу.
- Делаем финальный инкрементальный
rsyncизменений + свежий дамп базы (окно в секунды), переключаем DNS на новый хост. - Старый хост не гасим сразу — он живёт как страховка на случай, если на новом что-то не так, и вы переключаете DNS обратно (это и есть самый быстрый откат без работы с файлами).
В этом сценарии пользователи почти не видят простоя: старые кэши DNS держат старый хост живым, новые запросы идут на новый, а между ними сайт отвечает с обоих одинаково, потому что оба собраны из одной копии.
Чек-лист граблей (по всему тексту)
- Держим копию **только на том же диске** → потеря диска/данных и копии разом. Нужен off-site (3-2-1).
- Дампаем без `—single-transaction` → блокировка таблиц = заметный даунтайм живой базы.
- Дампаем с `—single-transaction` по таблице **MyISAM** → флаг не защищает; проверить движок таблиц (запрос в Части II).
- Забыли `—routines —triggers —events` → потеряли логику БД (WP-плагины её активно используют).
- Не сверили момент снимков БД и файлов → после отката рассинхрон картинок и записей (особенно видно при миграции).
- Бэкап снимается вручную → через две недели забыт; автоматизация по таймеру обязательна.
- Один синхронизируемый `rsync —delete` без ротации → случайное удаление на оригинале через сутки стирает и «копию»; держим несколько меток-снимков.
- Ключ SSH для off-site имеет права на запись в боевой сайт → компромисс копии = компромисс прода. Отдельный ключ только под `/backups`.
- Никогда не проверяли восстановление → узнали что дамп битый в момент аварии. Тест восстановления раз в квартал обязателен.
- Откатываем «и базу, и файлы, и конфиг» одним шагом → не поймёте какой слой сломан; меняем один слой за раз (правило Диагностика).
- Не включили режим обслуживания при импорте → «белый экран» вместо предсказуемого «техработы».
👤 Зарегистрируйтесь, чтобы добавить материал в закладки.
