До этого момента бот жил на вашем ноутбуке: окно терминала закрыто — бот мёртв, и пользователи знают это лучше вас. Деплой на 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.targetAfter и 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, и по умолчанию журнал не сожрёт диск.
Два симметричных инцидента. Первый: бот завис, но не упал — цикл GetUpdates застрял на запросе без таймаута, процесс жив, Restart=always бессилен, потому что перезапускать нечего: systemd видит активный сервис. Лечение на стороне кода — таймауты на HTTP-клиенте из урока про ошибки и лимиты и контрольная точка внутри цикла, — но и в мониторинге есть приём: бот раз в минуту пишет в лог метку живости, и простая проверка journalctl на свежесть последней строки ловит зависание. Второй инцидент обратный: бот падает каждый час, systemd честно поднимает, RestartSec=5, — а пользователи видят перерывы, и никто не в курсе, потому что логи смотрели только когда «что-то не так». Команда systemctl status habitbot показывает счётчик рестартов и последний краш-дамп сразу — привычка смотреть его после любого деплоя стоит одну минуту и закрывает половину сюрпризов.
Обновление за секунды и откат#
Обновление живого бота — это смена файла и рестарт, и порядок действий делает простой коротким или долгим. Медленный путь: скопировать бинарник поверх работающего — на некоторых файловых системах запись в исполняемый файл во время работы даёт ошибку или падение сервиса. Правильный путь из схемы выше: новый файл кладётся как 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-pagerset -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 и без лишних денег.
Сборка и systemd требуют локального Go и линукс-машины: песочница самоучителя ограничена fmt, strings, strconv, math, sort, errors и time, а юниты проверяются на реальном сервере или бесплатной виртуалке. Локально с Linux или WSL юнит поднимается без VPS — путь к файлу меняется на домашний каталог, и весь цикл сборка-юнит-journalctl проходит на ноутбуке. Дешёвый тест живучести до сервера: запустить бота и убить процесс с kill -9 — при юните systemd поднимет его за RestartSec, и это видно в status счётчиком рестартов.
Проверка: автозапуск и падение
Бот упал с паникой в горутине на VPS с юнитом Restart=always. Что произойдёт?
Проверка: секреты в юните
Где хранить токен бота на сервере?
Старт до появления сети
Прочитайте код и выберите, что он выведет:
[Unit]
Description=Habit tracker bot
# строк After и Wants network-online.target НЕТ
[Service]
ExecStart=/opt/habitbot/habitbot
Restart=always
RestartSec=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 окно времени, когда бот упал, и напишите себе одну строку алерта по свежести последней записи лога.
Личные заметкиПусто
Вопросы и ответы по уроку
Нужен ли Docker для бота на Go?
Нет: бинарник самодостаточен, а systemd закрывает автозапуск, рестарты и логи. Docker оправдан, когда на сервере уже есть контейнерная инфраструктура или бот тянет за собой Redis и свой фронтенд и хочется описать это одной композицией — для одиночного процесса это лишний слой.
Long polling или webhook после переезда на VPS?
Оба работают, но webhook на VPS естественнее: домен, сертификат и апдейты по HTTPS описаны в уроке про webhook, а long polling остаётся рабочим вариантом без домена. Смена режима делается вызовом соответствующего метода API и не меняет логику хендлеров.
Что происходит с сообщениями во время простоя?
Telegram хранит апдейты и отдаёт их после восстановления: при long polling бот получит накопившееся первым же GetUpdates, при webhook доставка повторяется, пока сервер не ответит успешно. Обработчики должны переживать повторную доставку — идемпотентность по идентификаторам из уроков про платежи и хранение.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Мониторинг Go с Prometheus: метрики, Grafana, алерты
Курс по метрикам в Go: Counter, Gauge, Histogram, лейблы, PromQL, Grafana-дашборды, алерты и бизнес-метрики. 25 уроков с примерами client_golang.
- Понимать четыре типа метрик и выбирать правильный
- Экспортировать метрики из Go через promhttp
- Инструментировать HTTP-запросы и бизнес-события
- Писать PromQL-запросы: rate, sum, p99