Бот, запущенный в screen или через nohup, останавливается при первой ошибке и не поднимается после перезагрузки сервера. Ниже — как развернуть Telegram-бота на сервере правильно: как службу systemd, которая стартует вместе с системой, перезапускается после сбоя, пишет логи в системный журнал и работает от отдельного пользователя без лишних прав. Разберём бота на Python и на Node.js — отличается только установка и одна строка в настройках службы.
Проверено на Ubuntu 24.04 и 26.04: Python 3.12 и 3.14 с aiogram 3.31.0, Node.js 24.21.0 с grammY 1.46.0, systemd 255 и 259. Код ботов проверен на тестовом сервере, который отвечает как Bot API Telegram.
Что нужно заранее
- Сервер с Ubuntu 24.04 или 26.04 и пользователь
adminс правамиsudo— как его создать, описано в первой настройке сервера. - Токен бота от @BotFather — строка вида
123456789:AA…. - Код бота, лучше в git-репозитории. Если кода пока нет, возьмите пример из статьи.
Бота будем называть echobot: служба echobot.service, пользователь echobot, каталог /opt/echobot. Подставьте своё имя.
Шаг 1. Отдельный пользователь для бота
Бот не должен работать от root или от вашего пользователя: если в коде или библиотеке найдётся уязвимость, злоумышленник получит только права бота.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin echobot
id echobot
--system создаёт служебную учётную запись, nologin запрещает под ней входить. Код будет принадлежать admin — бот сможет его читать, но не изменять:
sudo install -d -o admin -g admin /opt/echobot
git clone https://github.com/you/echobot.git /opt/echobot # если код в git
Писать бот сможет только в каталог data — туда кладите всё, что он сохраняет в файлы (SQLite, загрузки). Добавьте data/ и .venv/ в .gitignore.
sudo install -d -o echobot -g echobot -m 750 /opt/echobot/data
Шаг 2. Python-бот: виртуальное окружение и aiogram
Возьмём aiogram — популярную асинхронную библиотеку для ботов на Python. На 11 октября 2026 года последняя версия — 3.31.0, она работает с Python 3.10–3.14, то есть с системным Python обеих версий Ubuntu.
sudo apt update
sudo apt install -y python3-venv git
cd /opt/echobot
python3 -m venv .venv
echo 'aiogram==3.31.0' > requirements.txt # если файла ещё нет в репозитории
.venv/bin/pip install -r requirements.txt
.venv/bin/python -c 'import aiogram; print(aiogram.__version__)'
Виртуальное окружение .venv — отдельная копия библиотек для этого проекта: обновление системных пакетов его не сломает. Точная версия в requirements.txt нужна, чтобы на сервере стояло то же, что вы проверили у себя.
Сам бот — файл /opt/echobot/bot.py. Он отвечает на /start и повторяет присланный текст:
import asyncio
import logging
import os
from aiogram import Bot, Dispatcher, F
from aiogram.filters import CommandStart
from aiogram.types import Message
dp = Dispatcher()
@dp.message(CommandStart())
async def start(message: Message) -> None:
await message.answer("Бот работает. Пришлите текст — я его повторю.")
@dp.message(F.text)
async def echo(message: Message) -> None:
await message.answer(message.text)
async def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
bot = Bot(token=os.environ["BOT_TOKEN"])
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Обратите внимание: токена в коде нет, бот берёт его из переменной окружения BOT_TOKEN. Время в формате логов не нужно — его добавит системный журнал.
Шаг 3. Токен — в отдельном файле с правами 600
Токен даёт полный доступ к боту. Его не пишут в код и не кладут в git — только в файл на сервере, который может прочитать один root:
sudo install -d -m 700 /etc/echobot
sudo install -m 600 /dev/null /etc/echobot/bot.env
sudo nano /etc/echobot/bot.env
В файл одна строка, без кавычек и пробелов вокруг =:
BOT_TOKEN=123456789:AA-ваш-токен
Редактор, а не echo … | sudo tee, — чтобы токен не остался в истории команд. Проверьте права: sudo ls -l /etc/echobot/bot.env должен показать -rw------- 1 root root. Systemd читает этот файл от имени root до запуска бота и передаёт значение в окружение процесса, поэтому самому пользователю echobot доступ к файлу не нужен — и его нет.
Шаг 4. Unit-файл systemd
Unit-файл описывает службу: что запускать, от чьего имени и что делать при сбое. Создайте /etc/systemd/system/echobot.service:
sudo nano /etc/systemd/system/echobot.service
[Unit]
Description=Echo Telegram bot
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=echobot
Group=echobot
WorkingDirectory=/opt/echobot
EnvironmentFile=/etc/echobot/bot.env
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/echobot/.venv/bin/python bot.py
Restart=on-failure
RestartSec=5
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/opt/echobot/data
ProtectHome=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
Что здесь важно:
After=иWants=network-online.target— запускать бота после того, как поднялась сеть, а не просто после старта сетевой службы.EnvironmentFile— файл с токеном из шага 3.PYTHONUNBUFFERED=1— без этой строки Python копит выводprint()в буфере, и в журнале его не видно. Мы проверили: без неё строкиprint()в журнал не попали.ExecStart— полный путь к Python из виртуального окружения; активировать окружение отдельно не нужно.Restart=on-failure— перезапуск, если процесс завершился с ошибкой или его убили. Если бот завершился сам с кодом 0, systemd его не перезапускает. Нужен перезапуск в любом случае — пишитеRestart=always.RestartSec=5— пауза перед перезапуском. По умолчанию systemd прекращает попытки после 5 запусков за 10 секунд; с паузой в 5 секунд этот предел не достигается, и бот перезапускается, пока не заработает.
Последний блок — ограничения, которые почти ничего не стоят и сильно сужают возможности взломанного бота:
NoNewPrivileges=yes— процесс и его потомки не могут получить больше прав, например черезsudo.ProtectSystem=strict— вся файловая система для бота только для чтения, кроме каталогов изReadWritePaths.ProtectHome=yes— каталоги/homeи/rootдля бота недоступны.PrivateTmp=yes— у бота свой/tmp, не общий с другими программами.
Проверьте файл на ошибки:
sudo systemd-analyze verify /etc/systemd/system/echobot.service
Пустой вывод — ошибок нет.
Шаг 5. Запуск и автозапуск
sudo systemctl daemon-reload
sudo systemctl enable --now echobot
systemctl status echobot
daemon-reload перечитывает unit-файлы, enable --now включает автозапуск при загрузке и сразу запускает бота. В выводе status должно быть Active: active (running) и enabled в строке Loaded. Напишите боту в Telegram /start — он ответит.
Мы проверили ограничения тестовым скриптом, запущенным в этой же службе. Запись в /opt/echobot/data работает; запись в /opt/echobot и /etc — Read-only file system; /home — Permission denied; sudo отвечает The "no new privileges" flag is set; файл /etc/echobot/bot.env бот прочитать не может, но переменная BOT_TOKEN у него есть.
Логи бота: journalctl
Всё, что бот пишет в консоль, попадает в системный журнал. Пользователь admin без группы adm журнала служб не видит (-- No entries --), поэтому команды — через sudo:
sudo journalctl -u echobot -f # смотреть в реальном времени, выход — Ctrl+C
sudo journalctl -u echobot -n 100 # последние 100 строк
sudo journalctl -u echobot --since "1 hour ago" # за последний час
sudo journalctl -u echobot --since today -g Error # только строки со словом Error
После запуска в журнале будут строки aiogram:
INFO aiogram.dispatcher: Start polling
INFO aiogram.dispatcher: Run polling for bot @echo_test_bot id=123456789 - 'Echo'
INFO aiogram.event: Update id=1 is handled. Duration 14 ms by bot id=123456789
Ошибки Python тоже приходят в журнал целиком, вместе с трассировкой. Фильтр -p err для них не поможет: всё, что программа пишет в консоль, журнал записывает с обычным приоритетом, поэтому ищите по тексту через -g.
Как проверить автоперезапуск
Имитируем сбой — убьём процесс бота:
sudo kill -9 $(systemctl show -p MainPID --value echobot)
sleep 7
sudo journalctl -u echobot -n 4 --no-pager
В журнале появятся строки Scheduled restart job, restart counter is at 1 и Started echobot.service. Сколько раз служба перезапускалась с момента запуска, покажет systemctl show echobot -p NRestarts.
Автозапуск после перезагрузки проверьте командой sudo reboot: после входа systemctl is-active echobot должен ответить active.
Как обновлять бота
Если код в git:
cd /opt/echobot
git pull --ff-only
.venv/bin/pip install -r requirements.txt
sudo systemctl restart echobot
sudo journalctl -u echobot -n 20 --no-pager
--ff-only не даёт git молча создать слияние, если на сервере кто-то правил файлы руками. Установка зависимостей нужна, только если менялся requirements.txt, но лишней она не будет. Последняя команда — убедиться, что новая версия запустилась без ошибок. Во время перезапуска бот не отвечает несколько секунд; сообщения, присланные за это время, Telegram хранит (до 24 часов) и отдаст после запуска.
Если меняли сам unit-файл, перед перезапуском выполните sudo systemctl daemon-reload.
Бот на Node.js: установка и запуск
В репозитории Ubuntu версии Node.js устаревшие: в 24.04 — 18.19 (поддержка закончилась в апреле 2025 года), в 26.04 — 22.22. На 11 октября 2026 года актуальная LTS-версия — Node.js 24, она поддерживается до апреля 2028 года (график релизов). Node.js 26 станет LTS 28 октября 2026 года — после этого можно ставить его, заменив 24 на 26 в адресе репозитория.
Поставим Node.js 24 из репозитория NodeSource:
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /etc/apt/keyrings/nodesource.asc
sudo tee /etc/apt/sources.list.d/nodesource.sources > /dev/null <<EOF
Types: deb
URIs: https://deb.nodesource.com/node_24.x
Suites: nodistro
Components: main
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/nodesource.asc
EOF
sudo apt update
apt policy nodejs # Candidate: 24.x.x-1nodesource1
sudo apt install -y nodejs
node -v # v24.21.0
Шаги 1 и 3 — пользователь, каталоги, файл с токеном — те же. Библиотека — grammY, версия 1.46.0:
cd /opt/echobot
npm init -y
npm install --save-exact grammy@1.46.0
Код — файл /opt/echobot/bot.mjs (расширение .mjs включает современный синтаксис import без настроек):
import { Bot } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
bot.command("start", (ctx) => ctx.reply("Бот работает. Пришлите текст — я его повторю."));
bot.on("message:text", (ctx) => ctx.reply(ctx.message.text));
bot.catch((err) => console.error("Ошибка при обработке update:", err.error));
process.once("SIGTERM", () => bot.stop());
process.once("SIGINT", () => bot.stop());
await bot.start({ onStart: (me) => console.log(`Бот @${me.username} запущен`) });
bot.catch не даёт ошибке в одном обработчике уронить весь бот. Обработка SIGTERM нужна, чтобы systemctl stop и restart останавливали бота аккуратно, а не по таймауту.
В unit-файле из шага 4 отличаются две строки:
Environment=NODE_ENV=production
ExecStart=/usr/bin/node bot.mjs
Остальное, включая ограничения, без изменений. Обновление — как для Python, только вместо pip команда npm ci --omit=dev: она ставит ровно то, что записано в package-lock.json.
Long polling или webhook
Оба бота выше работают через long polling: сами раз за разом спрашивают у Telegram новые сообщения (метод getUpdates). Это самый простой вариант: не нужны домен, открытые порты и сертификат, бот работает даже за NAT.
Webhook — обратная схема: Telegram сам отправляет каждое сообщение на ваш адрес. Для него нужны домен и HTTPS с действующим сертификатом на одном из портов 443, 80, 88 или 8443 (документация Bot API), обычно — веб-сервер перед ботом. Webhook имеет смысл при большом потоке сообщений или когда бот и так работает внутри веб-приложения. Для большинства ботов на одном сервере long polling достаточно.
Одновременно работает только один способ: пока у бота установлен webhook, getUpdates возвращает ошибку. grammY при запуске в режиме polling сам снимает webhook, aiogram — нет.
Если не работает
| Что видно | Что делать |
|---|---|
status=203/EXEC в systemctl status |
Неверный путь в ExecStart. Проверьте, что файл существует: ls -l /opt/echobot/.venv/bin/python или which node |
KeyError: 'BOT_TOKEN' (Python) или Empty token! (grammY) |
Нет переменной: проверьте путь в EnvironmentFile и формат строки BOT_TOKEN=… без пробелов |
Unauthorized в журнале, бот перезапускается каждые 5 секунд |
Неверный токен. Исправьте /etc/echobot/bot.env и выполните sudo systemctl restart echobot |
Conflict: terminated by other getUpdates request |
Тот же токен опрашивает другой процесс — например, бот запущен ещё и на вашем компьютере. Остановите второй экземпляр |
can't use getUpdates method while webhook is active |
У бота остался webhook. Снимите его: откройте https://api.telegram.org/bot<токен>/deleteWebhook |
Read-only file system или Permission denied при записи |
Бот пишет вне /opt/echobot/data. Перенесите файлы туда или добавьте каталог в ReadWritePaths |
В журнале нет вывода print() |
Нет строки Environment=PYTHONUNBUFFERED=1 |
journalctl отвечает -- No entries -- |
Запустите через sudo |
| Правки unit-файла не действуют | sudo systemctl daemon-reload, затем sudo systemctl restart echobot |
Какие ещё ограничения можно включить для службы, подскажет systemd-analyze security echobot: она перечисляет все настройки защиты и отмечает включённые.
Что дальше
- Если сервер новый, сначала пройдите первую настройку Ubuntu и настройте вход по SSH-ключу.
- Если у бота появилась база данных или веб-панель, удобнее запускать всё вместе в контейнерах — см. Docker Compose в продакшене на одном VPS.
- Сколько памяти занимает бот и как ограничить её для службы (
MemoryMax) — в статьях о размере сервера и о нехватке памяти. - Узнать, что бот упал, раньше пользователей — Uptime Kuma и уведомления в Telegram.
- Если бот — это цепочка «получить сообщение — обработать — ответить» без сложной логики, её можно собрать без кода в n8n на своём сервере.
- Подробнее о журнале служб и его размере — логи на сервере: journalctl и logrotate; установка и обновление Node.js (в Node 26 нет corepack) — как установить Node.js на сервер; веб-приложение на Python по той же схеме — FastAPI и Django на VPS.