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 навколо всього стека.

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

ai-assistants

Скільки коштує розробка AI асистента у 2026: RAG чатбот, база знань, CRM, Telegram та підтримка

Практичний гід для бізнесу: від чого залежить ціна розробки AI асистента у 2026 році, що входить у RAG чатбот, інтеграції з CRM, Telegram, guardrails, оцінювання, моніторинг і супровід.

blogs

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.

blogs

AI для розробки лендінгів: де він реально прискорює запуск, а де псує конверсію

Дослідження про використання AI у розробці лендінгів: v0, Webflow AI, Builder.io, Framer-подібні AI builders, генерація UX, copy, SEO, персоналізація, A/B тести, ризики шаблонності, безпеки, доступності та технічного боргу.

growth

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

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

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

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