Бэкапы и восстановление без даунтайма: как не потерять WordPress и уметь откатиться

Слово «даунтайм» (от англ. 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

Флаг снимает блокировку, но не всемогущ:

  1. Только для InnoDB. Если у вас таблицы движка MyISAM (старые установки или агрессивные плагины), --single-transaction их не защитит — такие таблицы дамп всё равно блокирует. Проверьте, что всё InnoDB:
-- sql — какие таблицы не InnoDB (вернут пустой список = всё чисто)
SELECT TABLE_NAME, ENGINE
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'wpdatabase' AND ENGINE <> 'InnoDB';
  1. Во время дампа нельзя менять схему. Запущенный прямо в окно снятия дампа ALTER TABLE (тяжёлый плагин обновился и меняет структуру) может дать конфликт. Окно дампа у базы в несколько сотен мегабайт — это десятки секунд, но в этот интервал не стоит ставить обновления (Обновления без простоя).
  2. Согласованность только внутри базы. Она не захватывает файлы. Следующая часть — как совместить.

Часть 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 набивается руками.

AI Cron Generator
Гость
Токены: — / 50 000
Гостевой лимит — 50 000. Зарегистрируйтесь, для пользователей лимит 200 000 токенов.
0 / 5000

Генератор снимает рутину формата расписания; само решение «как часто бэкапить и сколько хранить» остаётся за вами — см. чек-лист ниже.


Часть 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, а не рестарта сервиса. Порядок:

  1. Готовим новый хост заранее (nginx, PHP-FPM, MariaDB — см. Анатомия VPS, Nginx, PHP-FPM).
  2. Переливаем копию файлов + дамп базы на новый хост и проверяем его по IP (не доменом) — сайт поднимается и работает из копии.
  3. Снижаем TTL (time-to-live — «сколько жить записи») у DNS-записи заранее (за сутки до), чтобы переключение стало быстрым: пока TTL короткий, старые адреса кэшируются в браузерах недолго и трафик уходит на новый хост почти сразу.
  4. Делаем финальный инкрементальный rsync изменений + свежий дамп базы (окно в секунды), переключаем DNS на новый хост.
  5. Старый хост не гасим сразу — он живёт как страховка на случай, если на новом что-то не так, и вы переключаете DNS обратно (это и есть самый быстрый откат без работы с файлами).

В этом сценарии пользователи почти не видят простоя: старые кэши DNS держат старый хост живым, новые запросы идут на новый, а между ними сайт отвечает с обоих одинаково, потому что оба собраны из одной копии.


Чек-лист граблей (по всему тексту)

  • Держим копию **только на том же диске** → потеря диска/данных и копии разом. Нужен off-site (3-2-1).
  • Дампаем без `—single-transaction` → блокировка таблиц = заметный даунтайм живой базы.
  • Дампаем с `—single-transaction` по таблице **MyISAM** → флаг не защищает; проверить движок таблиц (запрос в Части II).
  • Забыли `—routines —triggers —events` → потеряли логику БД (WP-плагины её активно используют).
  • Не сверили момент снимков БД и файлов → после отката рассинхрон картинок и записей (особенно видно при миграции).
  • Бэкап снимается вручную → через две недели забыт; автоматизация по таймеру обязательна.
  • Один синхронизируемый `rsync —delete` без ротации → случайное удаление на оригинале через сутки стирает и «копию»; держим несколько меток-снимков.
  • Ключ SSH для off-site имеет права на запись в боевой сайт → компромисс копии = компромисс прода. Отдельный ключ только под `/backups`.
  • Никогда не проверяли восстановление → узнали что дамп битый в момент аварии. Тест восстановления раз в квартал обязателен.
  • Откатываем «и базу, и файлы, и конфиг» одним шагом → не поймёте какой слой сломан; меняем один слой за раз (правило Диагностика).
  • Не включили режим обслуживания при импорте → «белый экран» вместо предсказуемого «техработы».

👤 Зарегистрируйтесь, чтобы добавить материал в закладки.