PAS7 Studio
До всіх статей

Як задеплоїти Telegram-бота через webhook з Node.js, TypeScript, grammY, Nginx, SSL і systemd

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

08 квіт. 2026 р.· 7 хв читання· Туторіал
Кому підійдеBackend-розробникиNode.js-розробникиТі, хто вперше деплоїть Telegram-бота на VPSКоманди, які переносять бота з локального середовища в продакшнФаундери, яким потрібен простий деплой без зайвих шарів
Telegram-бот, задеплоєний на VPS через Node.js, TypeScript, grammY, Nginx, SSL і systemd

Цей туторіал веде до одного конкретного результату: Telegram-бот працює на VPS через справжній webhook, доступний по HTTPS, а після перезавантаження сервера процес піднімається автоматично.

Telegram-бот на TypeScript з grammY
Локальний процес, прив’язаний до 127.0.0.1
systemd-сервіс, який переживає перезапуск сервера
Nginx reverse proxy як єдина публічна точка входу
Сертифікат Let's Encrypt через Certbot
Telegram webhook, зареєстрований із secret token

Почни з чистої директорії на VPS. Застосунок буде на TypeScript, для Telegram використаємо grammY, а Express тут потрібен лише як тонкий HTTP-шар для webhook. Окремий process manager, крім systemd, не потрібен.

Створи проєкт і встанови залежності:

BASH
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

Створи структуру директорій:

BASH
mkdir -p src
touch tsconfig.json
touch .env.example
touch src/bot.ts
touch src/server.ts

Використай такий tsconfig.json:

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:

DOTENV
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:

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:

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.

01

Створи реальний environment-файл

Скопіюй .env.example у .env і впиши токен бота та довгий випадковий webhook secret.

02

Збери проєкт

Запусти npm run build, щоб скомпільований TypeScript з’явився в dist/.

03

Один раз запусти зібраний застосунок

Виконай npm start і переконайся, що процес стартує без конфігураційних помилок.

04

Перевір локальний health endpoint

Виконай curl http://127.0.0.1:3000/healthz і переконайся, що у відповіді є { "ok": true }.

Чому важлива саме така послідовність

Якщо локальний застосунок зламаний, Nginx і SSL лише ускладнять діагностику.

Тут краще використати рідний Linux service manager, який уже є в системі, а не додавати PM2 як ще одну рухому частину. Для одного процесу бота цього service file більш ніж достатньо.

Створи файл telegram-bot-webhook.service:

INI
[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 і запусти сервіс:

BASH
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

Подивись логи:

BASH
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:

NGINX
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:

BASH
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 і підготуй команду:

BASH
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot

Випусти сертифікат через Nginx flow:

BASH
sudo certbot --nginx -d bot.example.com

Перевір автоподовження:

BASH
sudo certbot renew --dry-run

Перевір публічний health endpoint:

BASH
curl https://bot.example.com/healthz

Не реєструй Telegram webhook, поки це не працює по HTTPS.

Що тут дає Certbot

Валідний сертифікат, HTTPS-конфігурацію в Nginx і значно чистіший перший прохід, ніж ручне збирання TLS-конфігів.

Тепер можна вказати Telegram фінальну публічну адресу. Секрет у setWebhook має точно збігатися з тим, що записаний у .env.

Зареєструй webhook:

BASH
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:

BASH
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 майже завжди і є джерелом проблеми.

Чому в цьому сетапі використано systemd, а не PM2?

Тому що на сервері вже є рідний Linux service manager. Для одного процесу бота systemd достатньо, щоб автоматично стартувати застосунок, перезапускати його після збоїв і піднімати після reboot.

Чому краще взяти grammY, а не працювати з сирими update-об’єктами Telegram вручну?

Тому що grammY нормально закриває модель апдейтів Telegram і дає команди та middleware-рівень, через що туторіал залишається практичним і не скочується в ручну Bot API-обв’язку на кожному кроці.

Чи потрібен Nginx, якщо застосунок і так слухає порт?

Так, для такого типу деплою він потрібен. Nginx виступає публічним HTTPS-краєм, а сам застосунок лишається приватним на localhost. Це прибирає TLS-термінацію і публічний routing із процесу бота.

Чи можна одночасно використовувати getUpdates і webhook?

Ні. Після активації outgoing webhook Telegram не дозволяє цьому самому боту отримувати апдейти через `getUpdates`.

Чи можна написати це все на звичайному JavaScript без TypeScript?

Так, можна. Але для нового Node.js-бота у 2026 році TypeScript — значно стабільніший базовий вибір, якщо проєкт має вирости далі за маленьке демо.

Ці джерела підтверджують вибір інструментів, поведінку webhook і підхід до Linux-сервісу, який використано в цьому туторіалі.

Перевірено: 08 квіт. 2026 р.Актуально для: Telegram Bot API webhooksАктуально для: Single-server VPS deploymentsАктуально для: Node.js та TypeScript застосункиАктуально для: Telegram-боти на grammYАктуально для: Nginx reverse proxy з Let's EncryptАктуально для: Linux-сервери з systemdПеревірено з: Node.js 24 LTSПеревірено з: TypeScriptПеревірено з: grammY webhook handlingПеревірено з: Express 5 request adapterПеревірено з: Nginx reverse proxyПеревірено з: Certbot for NginxПеревірено з: systemd service unitПеревірено з: Telegram Bot API `setWebhook`Перевірено з: Telegram Bot API `getWebhookInfo`

PAS7 Studio допомагає перейти від мінімального туторіального бота до реального продакшн-рішення: стабільний routing, Telegram-інтеграції, адмін-панелі, observability, фонові задачі, message flows і серверне hardening навколо всього стека.

Пов'язані статті

growth

AI SEO / GEO у 2026: ваші наступні клієнти — не люди, а агенти

Пошук зміщується від кліків до відповідей. Боти та AI-агенти сканують, цитують, рекомендують і дедалі частіше купують. Дізнайтесь, що таке AI SEO / GEO, чому класичного SEO вже недостатньо, і як PAS7 Studio допомагає брендам перемагати у «агентному» вебі.

blogs

Найпотужніший чіп від Apple? M5 Pro і M5 Max б'ють рекорди

Аналітичний розбір Apple M5 Pro і M5 Max станом на березень 2026 року. Пояснюємо, чому ці чіпи можна вважати найпотужнішими професійними ноутбучними SoC від Apple, як вони виглядають на тлі M4 Pro, M4 Max, M1 Pro, M1 Max і що показують у порівнянні з актуальними Intel та AMD.

blogs

Artemis II і код, який веде до Місяця

У цьому блозі розбираємо місію NASA Artemis II, яка стартувала 1 квітня 2026 року, і пояснюємо, що вона насправді говорить про сучасну інженерію: бортове ПЗ, резервні контури, симуляції, телеметрію, людський контроль і дуже обережну роль ШІ в космічній сфері.

telegram-media-saver

Автоматичне тегування та пошук збережених посилань

Інтеграція з GDrive/S3/Notion для автоматичного тегування та швидкого пошуку через пошукові API

Професійна розробка для вашого бізнесу

Створюємо сучасні веб-рішення та боти для бізнесу. Дізнайтеся, як ми можемо допомогти вам досягти цілей.