Автоматическое обновление игры без ручного вмешательства
Каждый, кто держал сервер на Rust, ARK или 7 Days to Die дольше пары месяцев, знает этот сценарий: разработчики выкатили патч в два часа ночи, а ты узнаёшь об этом утром по жалобам игроков, что сервер "висит на старой версии" или вообще не пускает клиентов из-за несовпадения билда. Ручное обновление — это будильник в неурочный час, вручную нажатый app_update и молитва, что ничего не отвалится. Ниже — рабочая связка скриптов, которая проверяет версию сама, предупреждает игроков заранее, аккуратно перезапускает сервер и откатывается назад, если апдейт что-то сломал.
Содержание
- Как понять, что вышло обновление, не трогая живой сервер
- Предупреждаем игроков заранее — без сюрпризов посреди рейда
- Backup перед апдейтом: страховка, без которой всё остальное бессмысленно
- Graceful restart: сохранить мир, а не просто убить процесс
- Сам апдейт: SteamCMD, валидация файлов и проверка успеха
- Откат при неудачном обновлении
- Собираем всё в расписание: cron или systemd timer
Как понять, что вышло обновление, не трогая живой сервер
Прежде чем что-то обновлять, нужно узнать buildid текущей установки и buildid в паблик-ветке Steam — и сравнить их, не выключая сервер. Текущий billid лежит в манифесте установки:
grep -m1 '"buildid"' /home/steam/rust/steamapps/appmanifest_258550.acf
Актуальный buildid паблик-ветки SteamCMD отдаёт через app_info_print, если предварительно обновить кэш приложения (app_info_update 1):
#!/bin/bash
# check_update.sh — сравнивает установленный buildid с публичным
APPID=258550 # Rust Dedicated Server, для другой игры — свой AppID
INSTALL_DIR=/home/steam/rust
CURRENT=$(grep -m1 '"buildid"' "$INSTALL_DIR/steamapps/appmanifest_${APPID}.acf" | grep -oE '[0-9]+')
LATEST=$(steamcmd +login anonymous +app_info_update 1 +app_info_print $APPID +quit \
| grep -A3 '"public"' | grep -m1 '"buildid"' | grep -oE '[0-9]+')
if [ "$CURRENT" != "$LATEST" ]; then
echo "update_available"
else
echo "up_to_date"
fi
Для игр без публичного AppID у выделенного сервера (некоторые тайтлы шарят билд с клиентской версией) ориентируйтесь на AppID именно dedicated-сборки — он почти всегда указан в официальной вики игры или в файле steam_appid.txt внутри установленной папки сервера. Если сомневаетесь в номере — не гадайте, а сверьтесь с официальными источниками разработчика, ошибка в AppID уронит совсем не ту игру.
Этот шаг — самый дешёвый по ресурсам: запрос к Steam API занимает пару секунд и не требует остановки процесса игры. Именно на нём строится вся автоматизация: обновление стартует только тогда, когда check_update.sh реально нашёл новый billid, а не по факту "прошло 24 часа".
Предупреждаем игроков заранее — без сюрпризов посреди рейда
Внезапный кик с сервера в разгар рейда или боссфайта — гарантированный повод для негатива в Discord. Стандартная практика — серия предупреждений через RCON с убывающим интервалом: за 30, 10 и 2 минуты до рестарта.
#!/bin/bash
# warn_players.sh — использует rcon-cli (https://github.com/gorcon/rcon-cli)
RCON_HOST=127.0.0.1
RCON_PORT=28016
RCON_PASS="your_rcon_password"
rcon_say() {
rcon-cli --host "$RCON_HOST" --port "$RCON_PORT" --password "$RCON_PASS" "say $1"
}
rcon_say "[Тех.перерыв] Обновление сервера через 30 минут. Заверши бой и сохранись."
sleep 1200
rcon_say "[Тех.перерыв] Обновление через 10 минут."
sleep 480
rcon_say "[Тех.перерыв] Обновление через 2 минуты. Сервер сохранит мир автоматически."
sleep 120
Для игр с консольными командами вместо RCON-протокола (например, часть модов на Project Zomboid или ARK через RCON-порт) синтаксис команды say может отличаться — проверьте её в документации конкретного мода или в статье про RCON-подключение и основные команды, там разобраны нюансы по разным играм. Если у вас уже настроен бот в Discord, логично дублировать эти же сообщения в игровой канал — тогда офлайн-игроки тоже увидят предупреждение и не удивятся, зайдя в момент рестарта; логика подключения такого бота отдельно разобрана в статье про Discord-бота для управления игровым сервером.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверBackup перед апдейтом: страховка, без которой всё остальное бессмысленно
Автообновление без бэкапа — это не автоматизация, а рулетка. Перед любым запуском app_update мир и конфиги должны улетать в отдельную папку с таймстампом:
#!/bin/bash
# backup_before_update.sh
INSTALL_DIR=/home/steam/rust
BACKUP_DIR=/home/steam/backups
TS=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR/$TS"
tar -czf "$BACKUP_DIR/$TS/server_data.tar.gz" \
-C "$INSTALL_DIR" server/my_server_identity
# храним только последние 10 бэкапов, чтобы не забить диск
ls -1dt "$BACKUP_DIR"/*/ | tail -n +11 | xargs -r rm -rf
Для игр с отдельной SQL-базой (панели статистики, часть плагинов Paper/Bukkit) бэкап мира без дампа базы бесполезен — добавьте mysqldump тем же шагом, до остановки сервиса. Если у вас уже есть регулярное расписание бэкапов помимо предобновленческого — не дублируйте логику, а вынесите общий код в отдельный скрипт и вызывайте его из обоих мест; подробная настройка расписания разобрана в статье про автобэкапы игрового сервера.
Graceful restart: сохранить мир, а не просто убить процесс
kill -9 по игровому процессу — верный способ повредить сейв на диске, особенно если игра пишет мир не атомарно. Правильная последовательность: команда сохранения из консоли игры → пауза, пока сейв реально запишется на диск → мягкая остановка процесса → обновление → запуск.
#!/bin/bash
# graceful_stop.sh
rcon-cli --host 127.0.0.1 --port 28016 --password "your_rcon_password" "save"
sleep 15 # дать время дописать сейв на диск, для больших миров — больше
# systemd-юнит с Type=simple и ExecStop, который сам шлёт "quit" в RCON перед SIGTERM
systemctl stop rust-server.service
# если процесс не запущен через systemd, а в screen/tmux — мягкий сигнал вместо -9
# pkill -SIGTERM -f RustDedicated
Если сервер до сих пор живёт в голом screen без systemd-юнита — это первое, что стоит поправить: без юнита нет предсказуемого ExecStop, и скрипт обновления не может быть уверен, что процесс действительно остановился, а не завис. Как оформить процесс сервера через systemd вместо screen/tmux — отдельная тема, разобранная в статье про systemd, screen и tmux для игровых процессов.
Сам апдейт: SteamCMD, валидация файлов и проверка успеха
Когда процесс остановлен и бэкап лежит на диске, можно запускать обновление. validate в конце команды заставляет SteamCMD сверить контрольные суммы всех файлов — это медленнее обычного апдейта, но ловит битые докачки после обрыва сети:
#!/bin/bash
# do_update.sh
STEAMCMD=/home/steam/steamcmd/steamcmd.sh
INSTALL_DIR=/home/steam/rust
APPID=258550
$STEAMCMD +force_install_dir "$INSTALL_DIR" \
+login anonymous \
+app_update $APPID validate \
+quit
UPDATE_EXIT=$?
if [ $UPDATE_EXIT -ne 0 ]; then
echo "SteamCMD вернул ошибку, апдейт прерван" >&2
exit 1
fi
Код завершения SteamCMD — не идеальный индикатор: в редких случаях он возвращает 0 даже при неполной докачке из-за проблем на стороне CDN Steam. Поэтому следующий шаг после апдейта — не просто "запустить сервер", а проверить, что он реально поднялся и отвечает на запросы (A2S-запрос или пинг по RCON), прежде чем считать обновление успешным. Общая логика "обновиться, но не потерять прогресс игроков" — с акцентом именно на сохранности сейвов при апдейте разных игр — подробно разобрана в статье про обновление сервера без потери прогресса игроков.
Откат при неудачном обновлении
Если после апдейта сервер не поднимается, крашится в первые минуты или ловит массовые ошибки в логе — держать игроков в неведении хуже, чем откатиться на предыдущую версию и разбираться в спокойном режиме.
#!/bin/bash
# rollback.sh
INSTALL_DIR=/home/steam/rust
BACKUP_DIR=/home/steam/backups
LATEST_BACKUP=$(ls -1dt "$BACKUP_DIR"/*/ | head -n1)
systemctl stop rust-server.service
tar -xzf "$LATEST_BACKUP/server_data.tar.gz" -C "$INSTALL_DIR"
systemctl start rust-server.service
sleep 30
if systemctl is-active --quiet rust-server.service; then
echo "Откат выполнен, сервер запущен на предыдущих данных."
else
echo "КРИТИЧНО: сервер не поднялся и после отката, нужна ручная проверка." >&2
fi
Важный нюанс: откат данных мира — это не откат бинарников игры. Если SteamCMD уже перезаписал исполняемые файлы на новую версию, а мир вы вернули на старый билд, часть модов и сейвов может конфликтовать по формату. Для проектов с активными модами храните отдельно и слепок папки с бинарниками (не только данные мира) — на дисках NVMe с MAATRIX GAMES место под пару лишних архивов почти всегда найдётся, экономить на этом не стоит.
Собираем всё в расписание: cron или systemd timer
Финальный шаг — обвязать все скрипты в единый пайплайн и повесить на расписание. Через cron:
# crontab -e
# Проверка каждый час, апдейт запускается только если check_update.sh нашёл новую версию
0 * * * * /home/steam/scripts/run_update_pipeline.sh >> /home/steam/logs/update.log 2>&1
#!/bin/bash
# run_update_pipeline.sh
if [ "$(/home/steam/scripts/check_update.sh)" = "update_available" ]; then
/home/steam/scripts/warn_players.sh
/home/steam/scripts/backup_before_update.sh
/home/steam/scripts/graceful_stop.sh
/home/steam/scripts/do_update.sh
systemctl start rust-server.service
sleep 30
if ! systemctl is-active --quiet rust-server.service; then
/home/steam/scripts/rollback.sh
fi
curl -s -H "Content-Type: application/json" \
-d "{\"content\":\"Обновление сервера завершено, статус: $(systemctl is-active rust-server.service)\"}" \
"$DISCORD_WEBHOOK_URL"
fi
Вариант через systemd timer вместо cron даёт более предсказуемое поведение при перезагрузке хоста (таймер сам подхватится после старта системы, не полагаясь на то, что cron-демон вообще запущен) — юнит update-pipeline.timer с OnCalendar=hourly и ссылкой на update-pipeline.service, который дёргает тот же run_update_pipeline.sh. Логика автозапуска по расписанию, включая нюансы OnCalendar, разобрана отдельно в статье про автоматический рестарт сервера по расписанию через cron — те же принципы, только вместо простого рестарта у нас условный запуск при наличии апдейта.
Ниже — сравнение трёх подходов к автообновлению по тому, насколько они безопасны для прод-сервера с живыми игроками:
| Подход | Плюсы | Минусы |
|---|---|---|
| SteamCMD автообновление на каждый запуск сервиса | Просто настроить, не требует скриптов | Обновляется в момент любого рестарта, без предупреждения игроков и без отката |
| Ручное обновление админом | Полный контроль над моментом апдейта | Требует присутствия человека, задержки при ночных патчах |
| Скрипт-пайплайн (этот гайд) | Проверка версии, предупреждение, бэкап, откат — без участия человека | Требует первоначальной настройки и тестового прогона |
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверЧастые вопросы
Можно ли обновляться сразу при выходе патча, без задержки на предупреждение игроков?
Технически да — уберите паузы в warn_players.sh. Но для серверов с активным онлайном это почти гарантированно вызовет недовольство: люди теряют прогресс боя или крафта без предупреждения. Минимум 10 минут форы — разумный компромисс.
Что если SteamCMD завис на середине докачки и не отвечает?
Добавьте timeout перед вызовом steamcmd в do_update.sh (например, timeout 1800 $STEAMCMD ...) и считайте ненулевой код выхода поводом для алерта в Discord, а не для автоматического отката — зависание сети не значит, что установленные файлы повреждены.
Нужно ли обновляться на каждый минорный патч или можно пропускать часть?
Зависит от игры: для Rust пропуск планового вайпового патча ломает совместимость с клиентами почти сразу, а для модовых серверов ARK иногда разумнее выждать пару дней и проверить, не сломал ли апдейт популярные моды, прежде чем катить его на прод.
Как быть с играми, где нет единого AppID выделенного сервера (например, часть модов Garry's Mod)?
В таких случаях версия сервера часто завязана на версию конкретного мода или воркшоп-айтема — check_update.sh нужно адаптировать под Workshop API конкретного проекта, общий AppID здесь не поможет.
Стоит ли запускать пайплайн на самом игровом хосте или лучше отдельно?
Для одиночного сервера разумнее держать скрипты локально — меньше сетевых задержек и точек отказа. Если серверов много, есть смысл вынести оркестрацию во внешний CI или в панель управления, дёргающую API хостинга, — это уже отдельная тема автоматизации через API.