Discord-бот для автоматического перезапуска сервера по расписанию
Плановый рестарт сервера — штука нужная: чистит память от утечек, подхватывает обновлённые конфиги, сбрасывает мелкие баги, которые копятся за сутки аптайма. Проблема в другом — если рестарт прилетает без предупреждения, игроки теряют прогресс, крафт, а иногда и нервы. Обычный cron с командой restart эту задачу не решает: он немой. Ниже — как собрать Discord-бота, который сам следит за расписанием, предупреждает в текстовом и голосовом канале, дёргает RCON и пишет результат в лог, чтобы вы не гадали, прошёл рестарт штатно или сервер лёг.
Содержание
Почему просто cron — не решение
Классический способ — cron-задача на хосте, которая раз в сутки шлёт sudo systemctl restart <server>.service. Он живёт своей жизнью, ничего не знает о Discord и не умеет предупредить игроков. Раньше эту дыру закрывали костылём: отдельный скрипт, который за 10 минут до рестарта дёргает вебхук в канал, а сам рестарт всё равно делает cron. Работает, но два независимых механизма — это два места, где может разъехаться расписание при переводе времени или ручной правке crontab.
Идея статьи — свести всё в одного бота: он сам держит расписание, сам считает обратный отсчёт, сам вызывает RCON и сам логирует итог. Никакого второго cron на хосте не нужно — точка правды одна.
Если вам не нужен именно "живой" бот с отсчётом в голосовом, а хватает тихого рестарта по расписанию — базовый вариант через cron и systemd разобран в статье про автоматический рестарт по расписанию. Здесь мы идём дальше и добавляем коммуникацию с игроками.
Стек, структура и .env
Бот пишется на discord.py (или py-cord — форк с более активной поддержкой voice на 2026 год) плюс библиотека для RCON-протокола. Понадобится:
- Python 3.11+ на машине, откуда бот будет ходить в RCON (можно прямо на сервере, можно с отдельной VPS — главное, чтобы RCON-порт был доступен).
- Токен Discord-приложения с правами
botиapplications.commands, включённый intentMESSAGE CONTENTв Developer Portal. - RCON-пароль и порт вашего игрового сервера — где их искать и как проверить подключение, подробно разобрано в статье про RCON: подключение и основные команды.
- Библиотека
rcon(пакетmcrconдля Minecraft или универсальныйrcon, работающий по протоколу Source RCON для Rust, ARK, CS2 и других игр на движке Source/Unreal). - APScheduler для гибкого cron-триггера внутри самого бота — надёжнее, чем ручной
asyncio.sleepв цикле.
python3 -m venv venv
source venv/bin/activate
pip install discord.py apscheduler rcon python-dotenv
Файл .env — храним секреты отдельно от кода, чтобы токен не улетел в git:
DISCORD_TOKEN=ваш_токен_бота
RCON_HOST=127.0.0.1
RCON_PORT=25575
RCON_PASSWORD=ваш_rcon_пароль
TEXT_CHANNEL_ID=123456789012345678
VOICE_CHANNEL_ID=123456789012345679
LOG_CHANNEL_ID=123456789012345680
RESTART_CRON=0 5 * * *
RESTART_CRON — время рестарта в формате cron (в данном примере — 5:00 по часовому поясу бота, ночное окно с минимумом онлайна). Файлы бота: bot.py, restart_manager.py, .env и лог restart.log в одной папке.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверЯдро: cron-триггер внутри бота
Вместо системного crontab используем AsyncIOScheduler из APScheduler — он живёт внутри event loop бота и не требует отдельного процесса на хосте.
# bot.py
import os
import logging
from datetime import datetime
import discord
from discord.ext import commands
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from dotenv import load_dotenv
from restart_manager import do_countdown_and_restart
load_dotenv()
logging.basicConfig(
filename="restart.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
scheduler = AsyncIOScheduler(timezone="Europe/Moscow")
@bot.event
async def on_ready():
print(f"Бот запущен как {bot.user}")
cron_expr = os.getenv("RESTART_CRON", "0 5 * * *")
minute, hour, day, month, dow = cron_expr.split()
trigger = CronTrigger(minute=minute, hour=hour, day=day, month=month, day_of_week=dow)
scheduler.add_job(scheduled_restart, trigger, id="scheduled_restart")
scheduler.start()
logging.info("Планировщик запущен, расписание: %s", cron_expr)
async def scheduled_restart():
await do_countdown_and_restart(bot)
bot.run(os.getenv("DISCORD_TOKEN"))
Обратите внимание на часовой пояс в AsyncIOScheduler(timezone=...) — это частая грабля: если оставить дефолт, бот будет считать по UTC, и рестарт "ночью в 5 утра" внезапно окажется днём.
Отсчёт в чате и статус в голосовом канале
Логика отсчёта и сам вызов RCON вынесены в отдельный модуль — так удобнее переиспользовать и для ручной команды !restart_now, и для планового триггера.
# restart_manager.py
import asyncio
import logging
import os
from rcon.source import rcon
TEXT_CHANNEL_ID = int(os.getenv("TEXT_CHANNEL_ID"))
VOICE_CHANNEL_ID = int(os.getenv("VOICE_CHANNEL_ID"))
LOG_CHANNEL_ID = int(os.getenv("LOG_CHANNEL_ID"))
RCON_HOST = os.getenv("RCON_HOST")
RCON_PORT = int(os.getenv("RCON_PORT"))
RCON_PASSWORD = os.getenv("RCON_PASSWORD")
WARNING_STEPS = [600, 300, 60, 10] # секунды до рестарта, когда шлём напоминание
async def do_countdown_and_restart(bot):
text_channel = bot.get_channel(TEXT_CHANNEL_ID)
log_channel = bot.get_channel(LOG_CHANNEL_ID)
await text_channel.send(
"⚠️ Плановый рестарт сервера через **10 минут**. Сохраните прогресс, найдите безопасное место."
)
await send_rcon_warning("Server restart in 10 minutes. Please find a safe spot.")
remaining = 600
for step in WARNING_STEPS[1:]:
await asyncio.sleep(remaining - step)
remaining = step
minutes = step // 60
text = f"{minutes} мин." if minutes else f"{step} сек."
await text_channel.send(f"⏳ Рестарт через **{text}**.")
await send_rcon_warning(f"Server restart in {text}.")
await asyncio.sleep(remaining)
await text_channel.send("🔄 Перезапускаю сервер...")
try:
response = await rcon("restart", host=RCON_HOST, port=RCON_PORT, passwd=RCON_PASSWORD)
logging.info("RCON restart отправлен, ответ: %s", response)
await text_channel.send("✅ Команда рестарта отправлена, сервер поднимется через 1-2 минуты.")
if log_channel:
await log_channel.send(f"[{bot.user}] Плановый рестарт выполнен успешно. Ответ RCON: `{response}`")
except Exception as e:
logging.error("Ошибка при рестарте: %s", e)
await text_channel.send("❌ Рестарт не удался, проверяю вручную.")
if log_channel:
await log_channel.send(f"⚠️ Ошибка планового рестарта: `{e}`")
async def send_rcon_warning(message: str):
try:
await rcon(f"say {message}", host=RCON_HOST, port=RCON_PORT, passwd=RCON_PASSWORD)
except Exception as e:
logging.warning("Не удалось отправить внутриигровое предупреждение: %s", e)
Важный нюанс — команда restart не универсальна: у части серверных бинарников (например, чистый Source-движок) такой RCON-команды может не быть вовсе, и рестарт делается через quit + внешний watchdog (systemd с Restart=always), который поднимает процесс заново. Проверьте, что конкретно понимает ваш сервер, командой rcon "help" — не полагайтесь на то, что restart сработает "как везде".
Ловушка в названии темы — "голосовой отсчёт" не означает, что бот обязан зачитывать текст голосом в канале (это отдельная и куда более тяжёлая история с discord.py[voice], FFmpeg и TTS-движком). Практичнее и надёжнее — менять имя голосового канала на текущий статус, это видно всем, кто сейчас в войсе, без возни с аудио-стримом:
async def update_voice_status(bot, text: str):
voice_channel = bot.get_channel(VOICE_CHANNEL_ID)
if voice_channel:
try:
await voice_channel.edit(name=f"🎮 {text}")
except discord.HTTPException as e:
logging.warning("Не удалось переименовать голосовой канал: %s", e)
Учтите лимит Discord API — переименование канала ограничено примерно двумя изменениями в 10 минут, так что вызывайте update_voice_status только на ключевых шагах (за 10 минут, за 1 минуту, после рестарта), а не на каждом тике отсчёта. Полноценный голосовой бот, который заходит в канал и озвучивает статус — отдельная механика подключения к voice и стриминга аудио, за рамками этой статьи.
Ручной запуск и защита от случайного дубля
Полезно оставить возможность запустить тот же сценарий вручную — например, перед экстренным патчем, — и заблокировать повторный запуск, если рестарт уже идёт:
restart_lock = asyncio.Lock()
@bot.command(name="restart_now")
@commands.has_permissions(administrator=True)
async def restart_now(ctx):
if restart_lock.locked():
await ctx.send("Рестарт уже выполняется, подождите.")
return
async with restart_lock:
await ctx.send("Запускаю внеплановый рестарт.")
await do_countdown_and_restart(bot)
@restart_now.error
async def restart_now_error(ctx, error):
if isinstance(error, commands.MissingPermissions):
await ctx.send("Команда доступна только администраторам.")
commands.has_permissions(administrator=True) — минимальная защита, но для боевого сервера лучше явно проверять роль по ID через кастомный decorator, чтобы не зависеть от того, кто как назвал роли на сервере Discord.
Логирование и что делать, если рестарт "молчит"
Файл restart.log пишет каждый шаг — от старта планировщика до ответа RCON. Для долгоживущего бота лог стоит ротировать, иначе за пару месяцев файл разрастётся:
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler("restart.log", maxBytes=5_000_000, backupCount=3)
logging.basicConfig(handlers=[handler], level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
Отдельно стоит добавить проверку "сервер действительно поднялся" — RCON рапортует об успехе, но процесс иногда падает при старте из-за битого конфига или занятого порта:
async def verify_server_up(bot):
await asyncio.sleep(90)
log_channel = bot.get_channel(LOG_CHANNEL_ID)
try:
response = await rcon("list", host=RCON_HOST, port=RCON_PORT, passwd=RCON_PASSWORD)
await log_channel.send(f"✅ Сервер отвечает после рестарта: `{response}`")
except Exception:
await log_channel.send("🚨 Сервер не отвечает через 90 секунд после рестарта — нужна ручная проверка!")
Команда list — универсальный "пинг" (список игроков онлайн), который поддерживают почти все RCON-реализации, но название может отличаться в вашей сборке — сверьтесь с документацией движка.
Запуск как systemd-сервис
Чтобы бот не падал вместе с закрытой SSH-сессией и поднимался сам после перезагрузки хоста, оформите его unit-файлом:
# /etc/systemd/system/discord-restart-bot.service
[Unit]
Description=Discord bot for scheduled server restarts
After=network.target
[Service]
Type=simple
User=gameadmin
WorkingDirectory=/home/gameadmin/discord-restart-bot
ExecStart=/home/gameadmin/discord-restart-bot/venv/bin/python bot.py
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now discord-restart-bot.service
sudo systemctl status discord-restart-bot.service
Restart=on-failure — важная деталь: если сам бот упадёт (например, Discord API временно недоступен), systemd поднимет его снова, и запланированный APScheduler-джоб восстановится при следующем on_ready.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверЧастые вопросы
Можно ли обойтись без Python и просто вебхуком из crontab?
Да, для минимального варианта — cron дёргает curl с вебхуком в канал за 10 минут, а следом отдельной командой RCON. Но тогда отсчёт будет грубым (одно сообщение, а не серия), и вы теряете единую точку логирования — придётся смотреть два места вместо одного.
Бот обязательно должен сидеть в голосовом канале?
Нет. Переименование канала-статуса — самый лёгкий и надёжный способ показать состояние "всем, кто в войсе", без подключения к аудио-потоку и без риска упереться в лимиты FFmpeg/opus.
Что если RCON-пароль или порт закрыты фаерволом?
Первым делом проверьте доступность порта с той машины, где крутится бот: nc -zv RCON_HOST RCON_PORT. Если бот и сервер на разных VPS — откройте порт только для IP бота, не для всего интернета, RCON без ограничения по IP — частая причина взлома сервера.
Расписание съехало после перехода на зимнее/летнее время?
Убедитесь, что timezone в AsyncIOScheduler указан явно (например, "Europe/Moscow"), а не оставлен по умолчанию в UTC — так CronTrigger сам учтёт переход, не потребуется править RESTART_CRON вручную дважды в год.
Нужно ли предупреждать игроков внутриигровым say, если уже есть Discord-сообщение?
Да, обязательно — часть игроков сидит в игре без открытого Discord, и say-команда через RCON — единственный способ достучаться до них напрямую в момент отсчёта.