Як задеплоїти Telegram-бота через webhook з Node.js, TypeScript, grammY, Nginx, SSL і systemd
Практичний туторіал 2026 року з деплою Telegram-бота на VPS через webhook з Node.js, TypeScript, grammY, Nginx, Let's Encrypt SSL і systemd. Є робочий код, реєстрація webhook і фінальні перевірки.

Висновок
Цей туторіал веде до одного конкретного результату: Telegram-бот працює на VPS через справжній webhook, доступний по HTTPS, а після перезавантаження сервера процес піднімається автоматично.
127.0.0.1Почни з чистої директорії на VPS. Застосунок буде на TypeScript, для Telegram використаємо grammY, а Express тут потрібен лише як тонкий HTTP-шар для webhook. Окремий process manager, крім systemd, не потрібен.
Створи проєкт і встанови залежності:
mkdir -p ~/apps/telegram-bot-webhook
cd ~/apps/telegram-bot-webhook
npm init -y
npm pkg set scripts.build="tsc -p tsconfig.json"
npm pkg set scripts.start="node dist/server.js"
npm install grammy express dotenv
npm install -D typescript @types/node @types/expressСтвори структуру директорій:
mkdir -p src
touch tsconfig.json
touch .env.example
touch src/bot.ts
touch src/server.tsВикористай такий tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}Використай такий .env.example:
HOST=127.0.0.1
PORT=3000
BOT_TOKEN=replace_with_your_bot_token
WEBHOOK_SECRET=replace_with_a_long_random_secret
WEBHOOK_PATH=/telegram/webhookЧому саме так
Такий підхід робить деплой читабельним. Код залишається компактним, залежності — зрозумілими, а серверна частина — близькою до того, як Linux-сервіси й так зазвичай запускаються.
Логіку бота і HTTP-вхідну точку краще розділити на два файли. Так transport-рівень webhook не змішується з командами та поведінкою самого бота.
Використай такий src/bot.ts:
import { Bot } from "grammy";
const botToken = process.env.BOT_TOKEN;
if (!botToken) {
throw new Error("BOT_TOKEN is required");
}
export const bot = new Bot(botToken);
bot.command("start", async (ctx) => {
await ctx.reply("Webhook is live. Your TypeScript bot is running.");
});
bot.command("ping", async (ctx) => {
await ctx.reply("pong");
});
bot.on("message:text", async (ctx) => {
if (ctx.message.text.startsWith("/")) {
return;
}
await ctx.reply("You said: " + ctx.message.text);
});Використай такий src/server.ts:
import "dotenv/config";
import express, { type NextFunction, type Request, type Response } from "express";
import { webhookCallback } from "grammy";
import { bot } from "./bot";
const host = process.env.HOST || "127.0.0.1";
const port = Number(process.env.PORT || 3000);
const webhookPath = process.env.WEBHOOK_PATH || "/telegram/webhook";
const webhookSecret = process.env.WEBHOOK_SECRET;
if (!webhookSecret) {
throw new Error("WEBHOOK_SECRET is required");
}
const app = express();
app.disable("x-powered-by");
app.use(express.json({ limit: "1mb" }));
app.get("/healthz", (_req: Request, res: Response) => {
res.status(200).json({ ok: true });
});
function verifyTelegramSecret(req: Request, res: Response, next: NextFunction): void {
const secret = req.header("x-telegram-bot-api-secret-token");
if (secret !== webhookSecret) {
res.status(403).json({ ok: false });
return;
}
next();
}
app.post(
webhookPath,
verifyTelegramSecret,
webhookCallback(bot, "express")
);
app.use((_req: Request, res: Response) => {
res.status(404).json({ ok: false });
});
async function start(): Promise<void> {
await bot.init();
app.listen(port, host, () => {
console.log("Telegram webhook app listening on http://" + host + ":" + port + webhookPath);
});
}
start().catch((error) => {
console.error("Failed to start bot:", error);
process.exit(1);
});У цьому коді є чотири важливі речі: застосунок слухає тільки localhost, має health endpoint, перевіряє секретний заголовок Telegram ще до запуску middleware і працює через grammY, а не через ручний розбір сирих update-об’єктів.
Що тут уже наближене до продакшну
Процес бота не світиться назовні, webhook-маршрут заданий явно, а запити Telegram відхиляються, якщо secret token не збігається.
Перш ніж чіпати Nginx чи сертифікати, переконайся, що сам застосунок працює локально на VPS.
Створи реальний environment-файл
Скопіюй .env.example у .env і впиши токен бота та довгий випадковий webhook secret.
Збери проєкт
Запусти npm run build, щоб скомпільований TypeScript з’явився в dist/.
Один раз запусти зібраний застосунок
Виконай npm start і переконайся, що процес стартує без конфігураційних помилок.
Перевір локальний health endpoint
Виконай curl http://127.0.0.1:3000/healthz і переконайся, що у відповіді є { "ok": true }.
Чому важлива саме така послідовність
Якщо локальний застосунок зламаний, Nginx і SSL лише ускладнять діагностику.
Тут краще використати рідний Linux service manager, який уже є в системі, а не додавати PM2 як ще одну рухому частину. Для одного процесу бота цього service file більш ніж достатньо.
Створи файл telegram-bot-webhook.service:
[Unit]
Description=Telegram Bot Webhook
After=network.target
Wants=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/apps/telegram-bot-webhook
Environment=NODE_ENV=production
EnvironmentFile=/home/ubuntu/apps/telegram-bot-webhook/.env
ExecStart=/usr/bin/node /home/ubuntu/apps/telegram-bot-webhook/dist/server.js
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.targetСкопіюй його в systemd і запусти сервіс:
sudo cp telegram-bot-webhook.service /etc/systemd/system/telegram-bot-webhook.service
sudo systemctl daemon-reload
sudo systemctl enable --now telegram-bot-webhook
sudo systemctl status telegram-bot-webhookПодивись логи:
journalctl -u telegram-bot-webhook -fЗаміни User, WorkingDirectory, EnvironmentFile і ExecStart під свій сервер. Не копіюй шлях /home/ubuntu/... бездумно, якщо у тебе інша структура.
Що маємо на цьому етапі
Бот уже працює як звичайний Linux-сервіс і після reboot підніметься без будь-якого окремого Node-specific process manager.
Спочатку достатньо HTTP-конфігурації. Після цього Certbot зможе використати наявний Nginx site і сам додати HTTPS.
Створи Nginx site, наприклад /etc/nginx/sites-available/bot.example.com:
server {
listen 80;
listen [::]:80;
server_name bot.example.com;
location = /healthz {
proxy_pass http://127.0.0.1:3000/healthz;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /telegram/webhook {
proxy_pass http://127.0.0.1:3000/telegram/webhook;
proxy_http_version 1.1;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
return 404;
}
}Увімкни сайт і перезавантаж Nginx:
sudo ln -s /etc/nginx/sites-available/bot.example.com /etc/nginx/sites-enabled/bot.example.com
sudo nginx -t
sudo systemctl reload nginxМаршрут у Nginx має збігатися з маршрутом у .env і з тим шляхом, який ти пізніше зареєструєш у Telegram. Якщо хоча б десь шлях відрізняється, webhook не працюватиме.
Публічний край має бути простим
Із зовнішнього інтернету має бути доступний лише Nginx. Сам Node.js-застосунок має лишатися на 127.0.0.1.
На цьому етапі DNS уже має вказувати bot.example.com на публічну IP-адресу VPS, а 80 порт повинен бути доступний з інтернету.
Встанови Certbot і підготуй команду:
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbotВипусти сертифікат через Nginx flow:
sudo certbot --nginx -d bot.example.comПеревір автоподовження:
sudo certbot renew --dry-runПеревір публічний health endpoint:
curl https://bot.example.com/healthzНе реєструй Telegram webhook, поки це не працює по HTTPS.
Що тут дає Certbot
Валідний сертифікат, HTTPS-конфігурацію в Nginx і значно чистіший перший прохід, ніж ручне збирання TLS-конфігів.
Тепер можна вказати Telegram фінальну публічну адресу. Секрет у setWebhook має точно збігатися з тим, що записаний у .env.
Зареєструй webhook:
export BOT_TOKEN=replace_with_your_bot_token
curl -X POST "https://api.telegram.org/bot$BOT_TOKEN/setWebhook" \
-d "url=https://bot.example.com/telegram/webhook" \
-d "secret_token=replace_with_a_long_random_secret" \
-d 'allowed_updates=["message"]'Перевір стан webhook:
curl "https://api.telegram.org/bot$BOT_TOKEN/getWebhookInfo"Якщо все правильно, у полі url буде твоя HTTPS-адреса, а last_error_message буде порожнім або взагалі відсутнім.
Два значення, які мають збігатися
Шлях webhook і webhook secret повинні бути однаковими в застосунку, Nginx і Telegram.
Пройдися по цих перевірках саме по черзі, щоб одразу бачити, на якому рівні починається проблема.
systemd-сервіс активний
Спочатку перевір systemctl status telegram-bot-webhook, а вже потім підозрюй Nginx чи Telegram.
Локальний health endpoint працює
Перевір curl http://127.0.0.1:3000/healthz прямо на сервері.
Публічний HTTPS health endpoint працює
Перевір curl https://bot.example.com/healthz уже зовні.
Webhook info чистий
Подивись getWebhookInfo: URL має збігатися, а свіжих помилок доставки бути не повинно.
Команди в Telegram реально дають відповідь
Після активації webhook надішли боту /start і /ping.
Логи під час реального запиту залишаються читабельними
Поспостерігай за journalctl -u telegram-bot-webhook -f, поки надсилаєш повідомлення боту.
Як виглядає успішний результат
Повідомлення з Telegram доходить до бота через HTTPS, бот відповідає, а Linux-сервіс працює стабільно без ручного втручання.
Більшість проблем тут виникає через кілька повторюваних невідповідностей.
Бот запускають просто через npm start у SSH-сесії замість того, щоб створити нормальний Linux-сервіс.
Застосунок слухає публічний інтерфейс, а не 127.0.0.1.
У .env, Nginx і setWebhook використано різні webhook-шляхи.
У .env і в setWebhook прописані різні secret token.
Локальну перевірку health endpoint пропускають і намагаються дебажити весь стек одразу.
Webhook реєструють раніше, ніж запрацює HTTPS.
Конфіг Nginx змінюють і перезавантажують без nginx -t.
Поки активний webhook, намагаються працювати через getUpdates.
Що стоїть за більшістю збоїв
Неправильний порядок, неправильний шлях або неправильний secret майже завжди і є джерелом проблеми.
Тому що на сервері вже є рідний Linux service manager. Для одного процесу бота systemd достатньо, щоб автоматично стартувати застосунок, перезапускати його після збоїв і піднімати після reboot.
Тому що grammY нормально закриває модель апдейтів Telegram і дає команди та middleware-рівень, через що туторіал залишається практичним і не скочується в ручну Bot API-обв’язку на кожному кроці.
Так, для такого типу деплою він потрібен. Nginx виступає публічним HTTPS-краєм, а сам застосунок лишається приватним на localhost. Це прибирає TLS-термінацію і публічний routing із процесу бота.
Ні. Після активації outgoing webhook Telegram не дозволяє цьому самому боту отримувати апдейти через `getUpdates`.
Так, можна. Але для нового Node.js-бота у 2026 році TypeScript — значно стабільніший базовий вибір, якщо проєкт має вирости далі за маленьке демо.
Ці джерела підтверджують вибір інструментів, поведінку webhook і підхід до Linux-сервісу, який використано в цьому туторіалі.
PAS7 Studio допомагає перейти від мінімального туторіального бота до реального продакшн-рішення: стабільний routing, Telegram-інтеграції, адмін-панелі, observability, фонові задачі, message flows і серверне hardening навколо всього стека.
Пов'язані статті
AI SEO / GEO у 2026: ваші наступні клієнти — не люди, а агенти
Пошук зміщується від кліків до відповідей. Боти та AI-агенти сканують, цитують, рекомендують і дедалі частіше купують. Дізнайтесь, що таке AI SEO / GEO, чому класичного SEO вже недостатньо, і як PAS7 Studio допомагає брендам перемагати у «агентному» вебі.
Найпотужніший чіп від Apple? M5 Pro і M5 Max б'ють рекорди
Аналітичний розбір Apple M5 Pro і M5 Max станом на березень 2026 року. Пояснюємо, чому ці чіпи можна вважати найпотужнішими професійними ноутбучними SoC від Apple, як вони виглядають на тлі M4 Pro, M4 Max, M1 Pro, M1 Max і що показують у порівнянні з актуальними Intel та AMD.
Artemis II і код, який веде до Місяця
У цьому блозі розбираємо місію NASA Artemis II, яка стартувала 1 квітня 2026 року, і пояснюємо, що вона насправді говорить про сучасну інженерію: бортове ПЗ, резервні контури, симуляції, телеметрію, людський контроль і дуже обережну роль ШІ в космічній сфері.
Автоматичне тегування та пошук збережених посилань
Інтеграція з GDrive/S3/Notion для автоматичного тегування та швидкого пошуку через пошукові API
Професійна розробка для вашого бізнесу
Створюємо сучасні веб-рішення та боти для бізнесу. Дізнайтеся, як ми можемо допомогти вам досягти цілей.