Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 24 из 25 ~18 мин +20 XP

Деплой на VPS: systemd и логи

Деплой телеграм-бота на VPS — это один скомпилированный бинарник, systemd-юнит с автозапуском и journalctl для логов: Go собирает всё в один файл, и облак с Docker для бота не нужен. Разберём сборку под линукс-сервер, юнит с Restart=always и EnvironmentFile, чтение логов через journalctl и обновление, при котором бот простаивает секунды.

Содержание урока · 8 разделов

До этого момента бот жил на вашем ноутбуке: окно терминала закрыто — бот мёртв, и пользователи знают это лучше вас. Деплой на VPS превращает скрипт в сервис, и в случае Go процесс короткий до смешного: код собирается в один статический бинарник, файл копируется на сервер, systemd запускает его и перезапускает при падении. Контейнеры, оркестраторы и облака здесь избыточны: у бота один процесс, одна зависимость — сеть до api.telegram.org — и ноль файловой структуры вокруг. Но «просто запустить» недостаточно: бот без автозапуска умрёт при первой перезагрузке сервера, токен в командной строке процесса виден всем пользователям через ps, а логи в nohup.out теряются и гниют. Этот урок собирает деплой по шагам с кодом юнита и командами, а по пути разбирает две классические ошибки: бот, который молчит после падения, и обновление, которое кладёт сервис на минуты.

Сборка: GOOS=linux go build#

Кросс-компиляция — встроенная способность Go-тулчейна: одна переменная окружения перед сборкой говорит, под какую платформу собирать. С ноутбука под macOS или Windows сервер с линуксом собирается командой GOOS=linux go build -o habitbot ./cmd/bot: флаг -o задаёт имя бинарника, а GOARCH=amd64 обычно совпадает с сервером по умолчанию — на ARM-машинах, вроде дешёвых VPS на ARM, ставится GOARCH=arm64. Полученный файл самодостаточен: рантайм, все зависимости и даже частично запрошенные ресурсы внутри, на сервере не нужен ни Go, ни модули, ни библиотеки — scp и запуск. Сборке стоит заранее добавить версию: флаг -ldflags "-X main.version=1.4.0" вшивает строку в переменную, бот отвечает /version реальным номером, и вопрос «что сейчас на проде» решается одним сообщением боту, а не поиском по файлам. Честная проверка перед выкладкой: собранный бинарник запускается локально с тем же флагом -version — ошибку компиляции под линукс дешевле увидеть у себя, чем на сервере.

Сборка и доставка на сервер

GO
// локально: кросс-сборка под линукс с версией внутри
GOOS=linux GOARCH=amd64 go build \
        -ldflags "-X main.version=1.4.0" \
        -o habitbot ./cmd/bot

// доставка и установка
scp habitbot deploy@203.0.113.10:/opt/habitbot/habitbot.new
ssh deploy@203.0.113.10 \
        "chmod +x /opt/habitbot/habitbot.new"

Обратные слэши в конце строк — перенос в шелле, сама сборка одна команда. Бинарник кладётся рядом с будущим сервисом под новым именем: habitbot.new, — и переименование в habitbot происходит в момент переключения из следующего раздела: так обновление занимает секунды. Пользователь deploy с правами на /opt/habitbot безопаснее, чем запуск сервиса от root.

Systemd-юнит: Restart=always и EnvironmentFile#

Systemd — менеджер сервисов в большинстве линукс-дистрибутивов, и для бота пишется маленький файл юнита в /etc/systemd/system/habitbot.service. Ключевые строки: ExecStart — абсолютный путь к бинарнику и его аргументы; Restart=always с RestartSec=5 — процесс умер или упал с паникой, systemd поднимет его через пять секунд, и это единственное, что отличает живучий бот от бота-инвалида; User=deploy — сервис работает не от root; EnvironmentFile=/etc/bot/env — файл с секретами, который юнит читает при старте. Файл окружения простой: строки КЛЮЧ=значение с токеном бота и настройками, права 600 и владелец deploy — токен не должен читаться никем другим. Важный нюанс: в EnvironmentFile нет кавычек и export — это не shell-скрипт, и попытка написать там export BOT_TOKEN=... закончится тем, что бот получит значение с мусором в начале. После правок юнита — systemctl daemon-reload, иначе systemd продолжит жить старой версией файла.

Юнит habitbot.service

GO
[Unit]
Description=Habit tracker bot
After=network-online.target
Wants=network-online.target

[Service]
User=deploy
WorkingDirectory=/opt/habitbot
ExecStart=/opt/habitbot/habitbot
EnvironmentFile=/etc/bot/env
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

After и Wants network-online.target ждут сеть перед стартом: боту нужен доступ к API Telegram, и старт до сети даёт падение на первом же GetUpdates. Restart=always покрывает и крах, и чистый выход: сервис обязан работать всегда. EnvironmentFile читается при каждом старте — смена токена не требует правок юнита, только рестарта сервиса.

Логи: journalctl вместо nohup.out#

Systemd собирает stdout и stderr сервиса в журнал journald, и читать его удобно командой journalctl -u habitbot: логи одного сервиса, с временными метками, без файлов на диске и ротаций руками. Полезные формы: journalctl -u habitbot -f — следить вживую, как tail -f; -n 100 — последние сто строк; --since today или --since "2025-01-20 10:00" — окно времени, когда пользователь пожаловался на бота. Из кода логи должны попадать в stdout или stderr: стандартный пакет log подходит, структурированные логгеры уровня zap из курса по zap тоже работают без изменений — journald заберёт всё. Правило для бота особое: логировать входящие ошибки API с кодами — 429 из урока про лимиты, отказы отправки из рассылок, — потому что по логам видно деградацию раньше пользователей. Ротация у journald встроенная: лимиты настраиваются в journald.conf, и по умолчанию журнал не сожрёт диск.

Обновление за секунды и откат#

Обновление живого бота — это смена файла и рестарт, и порядок действий делает простой коротким или долгим. Медленный путь: скопировать бинарник поверх работающего — на некоторых файловых системах запись в исполняемый файл во время работы даёт ошибку или падение сервиса. Правильный путь из схемы выше: новый файл кладётся как habitbot.new, затем две команды — mv habitbot.new habitbot и systemctl restart habitbot — простой ограничен секундами рестарта, а длинный polling-цикл сам переподключится. Для webhook-бота из урока про webhook рестарт ещё заметнее: Telegram шлёт апдейты на ваш адрес, и во время простоя они копятся в очереди Telegram — после подъёма всё доедет, ничего не теряется, если хендлер идемпотентен. Откат — тот же приём в обратную сторону: держите прошлый бинарник как habitbot.bak, и откат выглядит как mv плюс рестарт за те же секунды. Обновление с миграцией данных — редкость у ботов, но если появляется, миграция выносится в отдельный шаг до переключения, а не прячется в запуск.

Скрипт обновления целиком

GO
#!/bin/sh
set -e
cd /opt/habitbot
cp habitbot habitbot.bak          # прошлый бинарник для отката
mv habitbot.new habitbot
systemctl restart habitbot
systemctl status habitbot --no-pager | head -5
journalctl -u habitbot -n 20 --no-pager

set -e прерывает скрипт на первой ошибке: упавшая команда не даст выполниться рестарту с поломанным бинарником. status и journalctl в конце показывают сразу, поднялся ли сервис и что он написал при старте, — весь деплой превращается в scp плюс этот скрипт, и занимает меньше минуты вместе с проверкой.

Шпаргалка команд эксплуатации#

КомандаЧто делаетКогда нужна
systemctl start/stop/restart habitbotУправление сервисомДеплой и откат
systemctl status habitbotСостояние, PID, счётчик рестартовПервое, что смотрится при проблемах
journalctl -u habitbot -fЖивой хвост логовОтладка на месте
journalctl -u habitbot --since todayЛоги за периодРазбор жалобы пользователя
systemctl enable habitbotАвтозапуск при загрузкеОдин раз после установки юнита
daemon-reloadПеречитать юнитыПосле любой правки .service

Когда на сервере появляются второй и третий боты — боевой, тестовый, чужой пет-проект, — юниты копировать не нужно: systemd умеет шаблонные юниты. Файл habitbot@.service с символом at в имени превращается в любое число сервисов: systemctl start habitbot@prod и habitbot@test запускают два независимых процесса, и каждому передаётся свой экземпляр через спецификатор %i — путь к бинарнику вида /opt/habitbot/%i, файл окружения /etc/bot/env-%i. Тестовый бот со своим токеном из BotFather живёт рядом с боевым, обновляется первым и ловит поломки вместо пользователей; конфигурация при этом не дублируется, а порождается из одного файла. Это же решение закрывает вопрос стейджинга: правка выкатывается в тестовый инстанс, прогоняется руками и тестами из урока про тестирование бота, и только затем переезжает в боевой — на одном сервере, без второго VPS и без лишних денег.

Проверка: автозапуск и падение

Бот упал с паникой в горутине на VPS с юнитом Restart=always. Что произойдёт?

Проверка: секреты в юните

Где хранить токен бота на сервере?

Старт до появления сети

Прочитайте код и выберите, что он выведет:

[Unit]
Description=Habit tracker bot
# строк After и Wants network-online.target НЕТ

[Service]
ExecStart=/opt/habitbot/habitbot
Restart=always
RestartSec=5

Соберите ключевые строки юнита

Впишите директивы перезапуска, файла окружения и цели автозапуска.

[Service] Restart= RestartSec=5 EnvironmentFile= User=deploy [Install] WantedBy=

Соберите порядок обновления бота

Расставьте команды: от сборки до проверки живого сервиса.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: деплой бота

Термин

GOOS=linux go build

нажмите, чтобы перевернуть
Определение

Кросс-сборка под сервер с ноутбука: один статический бинарник без зависимостей, доставка — scp. Версия вшивается через -ldflags -X.

1 / 5

Связи темы: урок про webhook — режим, при котором Telegram сам шлёт апдейты на ваш домен, и деплой получает ещё и сертификат; урок про ошибки и лимиты — таймауты, без которых бот зависает живым; урок про напоминания — фоновая горутина, чьё состояние обязано переживать рестарт; урок про хранение данных — Redis и файловое хранилище под это. Перед выкладкой: урок про тесты бота — интеграционный прогон, и итоговый проект курса — трекер привычек, который деплоится по этой схеме. Полный маршрут — на странице курса.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Go-бот деплоится одним бинарником: GOOS=linux go build, scp на сервер, ноль зависимостей на машине.
  • Юнит systemd с Restart=always и RestartSec=5 превращает бот в сервис, который переживает падения и перезагрузку.
  • Токен живёт в EnvironmentFile с правами 600: без кавычек и export, меняется рестартом, а не пересборкой.
  • Логи — journalctl -u: живой хвост, окно времени и счётчик рестартов в systemctl status вместо nohup.out.
  • Зависший процесс Restart=always не лечит: таймауты в коде и метка живости в логах обязательны.
  • Обновление — .new, mv, restart, journalctl; откат теми же командами из habitbot.bak занимает секунды.

Домашнее задание

Соберите полный цикл деплоя: кросс-сборка с вшитой версией и командой /version в боте, юнит systemd с Restart=always, RestartSec=5 и EnvironmentFile=/etc/bot/env с токеном, enable для автозапуска. Проверьте живучесть: kill -9 процесса и убедитесь, что через пять секунд бот поднялся, а systemctl status показывает счётчик рестартов. Затем сделайте обновление через .new и mv с замером простоя, и откат через habitbot.bak. В конце найдите в journalctl окно времени, когда бот упал, и напишите себе одну строку алерта по свежести последней записи лога.

Личные заметкиПусто
Хранятся только в вашем браузере (Telegram · Деплой на VPS: systemd и логи)

Вопросы и ответы по уроку

Нужен ли Docker для бота на Go?

Нет: бинарник самодостаточен, а systemd закрывает автозапуск, рестарты и логи. Docker оправдан, когда на сервере уже есть контейнерная инфраструктура или бот тянет за собой Redis и свой фронтенд и хочется описать это одной композицией — для одиночного процесса это лишний слой.

Long polling или webhook после переезда на VPS?

Оба работают, но webhook на VPS естественнее: домен, сертификат и апдейты по HTTPS описаны в уроке про webhook, а long polling остаётся рабочим вариантом без домена. Смена режима делается вызовом соответствующего метода API и не меняет логику хендлеров.

Что происходит с сообщениями во время простоя?

Telegram хранит апдейты и отдаёт их после восстановления: при long polling бот получит накопившееся первым же GetUpdates, при webhook доставка повторяется, пока сервер не ответит успешно. Обработчики должны переживать повторную доставку — идемпотентность по идентификаторам из уроков про платежи и хранение.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Prometheus25 уроков~12 чпрактика25 уроков уже доступны

Мониторинг Go с Prometheus: метрики, Grafana, алерты

Курс по метрикам в Go: Counter, Gauge, Histogram, лейблы, PromQL, Grafana-дашборды, алерты и бизнес-метрики. 25 уроков с примерами client_golang.

  • Понимать четыре типа метрик и выбирать правильный
  • Экспортировать метрики из Go через promhttp
  • Инструментировать HTTP-запросы и бизнес-события
  • Писать PromQL-запросы: rate, sum, p99