Ir al contenido

Buscar en la documentación

Busca en la documentación de Skiffly

Desplegar un bot de Telegram

Ejecuta un bot de Telegram en Skiffly como servicio worker — long polling con grammY (Node) o aiogram (Python), sin necesidad de dominio — y luego pasa a webhooks con un dominio generado, agrega Postgres para el estado y evita el doble polling durante los despliegues.

Qué vas a construir: un servicio bot desde un repositorio de GitHub que ejecuta un bot de Telegram con long polling (no requiere URL pública), con su token en una variable y Postgres para el estado. Después, la variante con webhooks. Unos 10 minutos.

Necesitas: un token de bot de @BotFather, la CLI skiffly y un repositorio con uno de los ejemplos de abajo.

1. El bot#

src/bot.ts
import { Bot } from "grammy";
 
const bot = new Bot(process.env.BOT_TOKEN!);
bot.command("start", (ctx) => ctx.reply("Hello from Skiffly"));
bot.on("message:text", (ctx) => ctx.reply(ctx.message.text));
 
process.once("SIGTERM", () => bot.stop());
process.once("SIGINT", () => bot.stop());
bot.start();
package.json
{ "scripts": { "start": "node --import tsx src/bot.ts" }, "dependencies": { "grammy": "^1", "tsx": "^4" } }

Railpack ve package.json y el script start; no hay paso de build.

Haz push del repositorio a GitHub.

2. Desplegar como worker#

skiffly init --name telegram-bot
skiffly variables set --skip-deploys BOT_TOKEN=123456:ABC…     # después de que `skiffly up -y -d` haya creado el servicio, o configúralo primero en el panel
skiffly up
skiffly logs -f

Resultado esperado: el build termina, el log de ejecución se queda en silencio (grammY) o imprime Start polling (aiogram), y el despliegue queda en SUCCESS. Para el polling no hace falta dominio. Pero hay un detalle:

src/bot.ts
import { createServer } from "node:http";
createServer((_, res) => res.end("ok")).listen(Number(process.env.PORT ?? 8080), "0.0.0.0");

Configura la ruta del health check como /healthz en Configuración → Despliegue (o healthcheckPath), vuelve a desplegar y envíale /start al bot.

3. Un solo poller a la vez#

Telegram permite un solo consumidor de getUpdates por token. Durante un rollout, el contenedor viejo sigue corriendo hasta que el nuevo está saludable, así que durante unos segundos dos instancias hacen polling y Telegram responde 409 Conflict a una de ellas. Ambas bibliotecas reintentan, así que es inofensivo; pero mantén las réplicas en 1 y no ejecutes el mismo token en local mientras el servicio esté activo. Si quieres cero solapamiento, usa webhooks.

4. Webhooks en lugar de polling#

Los webhooks necesitan una URL HTTPS pública, que un dominio generado proporciona:

skiffly domain            # https://telegram-bot-x1y2.skiffly.cloud
src/bot.ts
import { Bot, webhookCallback } from "grammy";
import { createServer } from "node:http";
 
const bot = new Bot(process.env.BOT_TOKEN!);
bot.command("start", (ctx) => ctx.reply("Hello via webhook"));
 
const handle = webhookCallback(bot, "http");
const url = `https://${process.env.SKIFFLY_PUBLIC_DOMAIN}/telegram`;
createServer((req, res) => {
  if (req.url === "/healthz") return res.end("ok");
  if (req.url === "/telegram" && req.method === "POST") return handle(req, res);
  res.statusCode = 404; res.end();
}).listen(Number(process.env.PORT ?? 8080), "0.0.0.0", async () => {
  await bot.api.setWebhook(url, { secret_token: process.env.WEBHOOK_SECRET });
});

SKIFFLY_PUBLIC_DOMAIN se define automáticamente en cuanto existe el dominio; agrega WEBHOOK_SECRET como variable (openssl rand -hex 16). Vuelve a desplegar y comprueba con curl https://api.telegram.org/bot<token>/getWebhookInfo. El modo webhook también permite que funcione App Sleeping (el primer mensaje tras la inactividad despierta al bot en unos segundos); los bots con polling deben permanecer despiertos.

5. Estado en Postgres#

skiffly deploy --template postgres
skiffly variables set 'DATABASE_URL=${{Postgres.DATABASE_URL}}'

Crea las tablas desde el comando de inicio (sh -c "node scripts/migrate.js && node --import tsx src/bot.ts") o una sola vez con skiffly ssh -- node scripts/migrate.js. Para bots pequeños basta con SQLite en un volumen (skiffly volume add --mount-path /data, DB_PATH=/data/bot.sqlite): una réplica, que un bot tiene de todos modos.

6. Configuración como código#

.skiffly/skiffly.ts
import { defineSkiffly, github, postgres, preserve, project, service } from "@skiffly/config";
 
export default defineSkiffly(() => {
  const db = postgres("Postgres");
  const bot = service("bot", {
    source: github("acme/telegram-bot"),
    healthcheck: "/healthz",
    env: { BOT_TOKEN: process.env.BOT_TOKEN ?? preserve(), DATABASE_URL: db.env.DATABASE_URL },
    replicas: 1,
    domain: true,      // modo webhook; quítalo para polling
  });
  return project("telegram-bot", { resources: [db, bot] });
});

Solución de problemas#

SíntomaSolución
Despliegue FAILED a los 5 min, aunque el bot respondía mensajes mientras tantono hay puerto de escucha: agrega el endpoint de salud y la ruta del health check
409: Conflict: terminated by other getUpdates request en el logotro poller (una ejecución local, una segunda réplica); inofensivo durante un rollout
401 UnauthorizedBOT_TOKEN incorrecto (revisa que no haya un salto de línea al final si lo configuraste con --stdin)
Webhook configurado pero no llegan updatesgetWebhookInfo muestra last_error_message: normalmente una ruta 404 o el dominio equivocado tras cambiar a un dominio propio
El bot deja de responder después de ~10 minutosApp Sleeping está activado en un bot con polling: desactívalo (sleepApplication: false)

Siguiente: Node.js · Python · Servicios: App Sleeping