Як задеплоїти 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 асистента у 2026: RAG чатбот, база знань, CRM, Telegram та підтримка
Практичний гід для бізнесу: від чого залежить ціна розробки AI асистента у 2026 році, що входить у RAG чатбот, інтеграції з CRM, Telegram, guardrails, оцінювання, моніторинг і супровід.
AI-assisted attacks і prompt injection у 2026: нова поверхня атак для продуктів з AI
Практичний гайд з безпеки AI-функцій: prompt injection, tool abuse, excessive agency, RAG poisoning, data leakage, output handling, human approval gates і permissions для AI agents.
AI для розробки лендінгів: де він реально прискорює запуск, а де псує конверсію
Дослідження про використання AI у розробці лендінгів: v0, Webflow AI, Builder.io, Framer-подібні AI builders, генерація UX, copy, SEO, персоналізація, A/B тести, ризики шаблонності, безпеки, доступності та технічного боргу.
AI SEO / GEO у 2026: ваші наступні клієнти — не люди, а агенти
Пошук зміщується від кліків до відповідей. Боти та AI-агенти сканують, цитують, рекомендують і дедалі частіше купують. Дізнайтесь, що таке AI SEO / GEO, чому класичного SEO вже недостатньо, і як PAS7 Studio допомагає брендам перемагати у «агентному» вебі.
Професійна розробка для вашого бізнесу
Створюємо сучасні веб-рішення та боти для бізнесу. Дізнайтеся, як ми можемо допомогти вам досягти цілей.