- PVSM.RU - https://www.pvsm.ru -
Год назад клиент написал в 23:47: «Сайт не открывается, говорит аккаунт заблокирован». Пока разбирались — выяснилось, что
Самое обидное: у клиента стояло «резервное копирование» через галочку в панели
После этого мы написали скрипт. Сейчас он с небольшими правками крутится на нескольких десятках клиентских сайтов.
На хостингах обычно доступно: SSH с ключом (chroot, без sudo), mysqldump, tar, gzip, lftp, curl. rsync — через раз, и об этом ниже.
Чего точно нет: root, apt install, /var/log в привычном виде, systemd. crontab через консоль — на части хостингов разрешён, на части только через веб-панель с ограниченным числом задач.
Честно — пробовали. rsync теоретически умеет работать через SSH, но без rsync-демона на принимающей стороне это значит «rsync к себе на тот же сервер», что бессмысленно. Если принимающая сторона — ваш
lftp с обычным FTP работает с первого раза и есть на любом
Панельные бэкапы — отдельная история. Либо «скачать архив вручную», либо «сохранить на тот же аккаунт». Смысл второго варианта я так и не понял, но кнопка выглядит внушительно.
[ shared-хостинг ] [ внешний FTP ]
public_html/ ──► tar.gz ──┐
├──► site-backups/
mysqldump ──► .sql ───┘ site-20260501-040015.tar.gz
▲
│ триггер по SSH
└──── [ VPS с cron ]
15 4 * * * ssh user@host '~/scripts/backup.sh'
Скрипт живёт на
Кладём в ~/scripts/backup.sh, строго выше public_html. Права chmod 700 — иначе другие пользователи
#!/bin/bash
set -u
SITE_DIR="/home/user/site/public_html"
WORK_DIR="/home/user/scripts"
LOG="$WORK_DIR/backup.log"
TMP_DIR="$(mktemp -d /tmp/bk.XXXXXX)"
DB_HOST="localhost"
DB_NAME="mydb"
DB_USER="mydb"
DB_PASS='secret-here'
FTP_HOST="ftp.example.com"
FTP_USER="ftpuser"
FTP_PASS='ftpsecret'
FTP_DIR="site-backups"
KEEP=30
TS="$(date +%Y%m%d-%H%M%S)"
ARCHIVE="site-${TS}.tar.gz"
ARCHIVE_PATH="$TMP_DIR/$ARCHIVE"
log() { printf '[%s] %sn' "$(date '+%F %T')" "$*" >> "$LOG"; }
trap 'rm -rf "$TMP_DIR"' EXIT
mkdir -p "$WORK_DIR"
log "=== backup start ($TS) ==="
# дамп базы
SQL_FILE="$TMP_DIR/db-${DB_NAME}-${TS}.sql"
MYSQL_PWD="$DB_PASS" mysqldump --single-transaction --quick
--routines --triggers --events --default-character-set=utf8mb4
-h "$DB_HOST" -u "$DB_USER" "$DB_NAME" > "$SQL_FILE" 2>>"$LOG"
|| { log "ERROR: mysqldump failed"; exit 1; }
log "mysqldump OK: $(stat -c %s "$SQL_FILE") bytes"
# архив файлов + дамп
tar --warning=no-file-changed -czf "$ARCHIVE_PATH"
--exclude='*/cache/*' --exclude='*/.git' --exclude='*/node_modules'
-C "$(dirname "$SITE_DIR")" "$(basename "$SITE_DIR")"
-C "$TMP_DIR" "$(basename "$SQL_FILE")" 2>>"$LOG" || true
[ -s "$ARCHIVE_PATH" ] || { log "ERROR: empty archive"; exit 1; }
log "archive: $(stat -c %s "$ARCHIVE_PATH") bytes"
# заливка + список файлов для ротации
lftp -u "$FTP_USER","$FTP_PASS" "$FTP_HOST" >>"$LOG" 2>&1 <<EOF
set ftp:ssl-allow no
set net:max-retries 3
mkdir -p $FTP_DIR
cd $FTP_DIR
put $ARCHIVE_PATH -o $ARCHIVE
cls -1 site-*.tar.gz > $TMP_DIR/list.txt
bye
EOF
log "upload OK: $ARCHIVE"
# ротация на FTP
TO_DEL=$(grep -E '^site-[0-9]{8}-[0-9]{6}.tar.gz$' "$TMP_DIR/list.txt"
| sort | head -n -"$KEEP")
if [ -n "$TO_DEL" ]; then
{
echo "set ftp:ssl-allow no"
echo "cd $FTP_DIR"
while IFS= read -r f; do echo "rm -f $f"; done <<< "$TO_DEL"
echo "bye"
} | lftp -u "$FTP_USER","$FTP_PASS" "$FTP_HOST" >>"$LOG" 2>&1
log "rotation: deleted $(echo "$TO_DEL" | wc -l)"
fi
log "=== backup done ==="
MYSQL_PWD вместо -p. Это не паранойя. Пароль в аргументе командной строки виден через ps aux всем пользователям сервера — а на shared их сотни. MYSQL_PWD читается mysqldump из окружения и в список процессов не попадает. Формально чище ~/.my.cnf с секцией [client] и chmod 600, но для одного скрипта оба варианта эквивалентны.
--single-transaction. Снимает консистентный снимок InnoDB без блокировки таблиц. Без него на живом магазине дамп может поймать транзакцию в промежуточном состоянии. Для MyISAM этот флаг ничего не даёт — но в 2026 году MyISAM это уже отдельная история.
--quick. Без него mysqldump загружает таблицу целиком в память и только потом дампит. На таблице с двумя миллионами строк это oom-killer и бэкап которого нет. Узнали не из документации.
--warning=no-file-changed у tar. На живом сайте файлы меняются прямо во время архивации — сессии, кеши. tar возвращает код 1 и пишет предупреждения, хотя архив нормальный. Без флага мониторинг считает бэкап упавшим. Флаг подавляет именно эти предупреждения, критические ошибки по-прежнему видны.
Cron из панели не видит mysqldump. cron-задача получает урезанный PATH, а mysqldump живёт не там где ожидается. Скрипт падает сразу и молча. Решение — export PATH=/usr/local/bin:$PATH в начало.
SSH-триггер глотает ошибки. ssh user@host 'script.sh' — если скрипт упал, снаружи не видно ничего без явного логирования. Пишем с обеих сторон: внутри скрипта в файл на
mkdir: Access failed: 550. lftp ругается при попытке создать уже существующую папку — mkdir -p в bash идемпотентен, в lftp нет. На сам бэкап не влияет, но выглядит страшно. Лечится добавлением set cmd:fail-exit no перед mkdir если нервирует.
Все архивы в корне FTP. Через полгода — 600 файлов непонятно от каких проектов. Делайте подпапки сразу: site1-backups/, shop-backups/. Ротация по маске site-*.tar.gz тогда не цепляет чужое.
Ротация на
Бэкап без проверки восстановления — это файлы на FTP с неизвестным содержимым. Звучит банально, но у двух клиентов из десяти бэкапы оказались нерабочими при первой проверке.
Мы делаем раз в месяц: скачиваем последний архив, разворачиваем локально в Docker, смотрим что сайт стартует и база читается. 15–20 минут. Раз в полгода — полное восстановление на тестовый аккаунт.
За три года так нашли два битых дампа: один раз кодировка utf8 вместо utf8mb4 резала кириллицу в именах, второй раз tar без -p потерял права на uploads/ и WordPress не мог сохранять файлы.
Шифрование: gpg --symmetric --batch --passphrase "$GPG_PASS" "$ARCHIVE_PATH" перед заливкой, грузить .tar.gz.gpg. Тогда компрометация FTP не означает слив данных.
Мониторинг без своего сервиса: в конец скрипта curl -s "https://hc-ping.com/uuid". Healthchecks.io бесплатно алертит, если пинг не пришёл вовремя.
restic или borg: если можно поставить бинарник на
Схема bash + lftp + внешний SSH-триггер работает на любом
Если у вас другой подход к бэкапам на shared — интересно услышать в комментариях.
Автор: paha66
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/backup/453129
Ссылки в тексте:
[1] хостинг: https://www.reg.ru/?rlink=reflink-717
[2] Источник: https://habr.com/ru/articles/1046948/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1046948
Нажмите здесь для печати.