Skip to content
BuildStackHQHosting
All docs

Docs

Bots

A Telegram or Slack bot as an always-on service.

A bot is an agent that never finishes: it waits for messages and answers them. On Keel that is an agent in service mode: the platform keeps one run of it up, restarts it when it crashes, gives it a persistent workspace for state such as the last processed update, and bills the hours it is up (Billing). This page deploys a Telegram bot twice, first with long polling (no inbound traffic needed), then behind a public hostname with a webhook.

Before you start

  • npm install -g @keelhosting/cli and keel login (The CLI).
  • A Telegram bot token from BotFather.
  • An organization on a plan with the services feature (keel usage shows the plan; business and enterprise include it). The webhook variant also needs the ingress feature.

Five commands: a long polling bot

mkdir support-bot && cd support-bot
keel init --template bot --name "Support Bot"
keel secrets set TELEGRAM_BOT_TOKEN
keel deploy
keel service start support-bot

What each step does:

  1. keel init --template bot writes keel.yaml, a Dockerfile, .dockerignore, bot.py and an empty requirements.txt. The bot uses only the Python standard library: it calls getUpdates with a 30 second long poll, answers every text message with You said: ..., keeps the update offset in /keel/workspace/offset so a restart never replays a message, and backs off (1 s doubling to 30 s) while Telegram is unreachable. The manifest is:

    mode: service
    workspace:
      mountPath: /keel/workspace
      sizeGb: 1
    egress:
      customHosts:
        - api.telegram.org
    secrets:
      - TELEGRAM_BOT_TOKEN
    

    Egress is deny-all by default, so api.telegram.org has to be listed. The token is a secret, never an env value: it is injected at start and never printed.

  2. keel secrets set TELEGRAM_BOT_TOKEN reads the token from a hidden prompt and stores it in the organization's secret store.

  3. keel deploy builds and pushes the image with Docker, creates the agent in service mode (POST /v1/agents with mode: service) and records the revision. The revision is refused with services_not_in_plan on a plan without services and with workspace_too_large when sizeGb exceeds the plan's workspaceGbPerAgent.

  4. keel service start support-bot brings the service up. keel service status support-bot --follow prints the state changes and the bot's stdout:

    service up run 2c1f...
    08:00:00.412 stdout  polling telegram from update 0
    

Send the bot a message in Telegram and watch the echo. Then try the failure path: keel service status support-bot shows Restarts, Last exit and Next restart after a crash, and keel logs support-bot reads the current run's stderr without knowing a run id.

Deploying again (keel deploy after editing bot.py) is a rolling restart: the platform starts a run on the new revision and ends the previous one; the workspace, and therefore the offset, is kept. keel service stop support-bot ends the run and stops the hourly billing; the workspace stays until the agent is archived.

The webhook variant: a public hostname

Long polling needs no inbound traffic, which is why it is the default. When Telegram (or any other system) has to call you, add ingress:

mkdir hook-bot && cd hook-bot
keel init --template webhook-bot --name "Hook Bot"
keel secrets set TELEGRAM_BOT_TOKEN
keel deploy
keel service start hook-bot
keel ingress hook-bot

The template differs in three places:

  • keel.yaml gains ingress: { port: 8080 }. Deploying it needs the ingress plan feature (ingress_not_in_plan otherwise).
  • bot.py runs a ThreadingHTTPServer on port 8080 that answers GET /healthz and accepts Telegram updates on POST /telegram. Only that path is served; everything else is 404.
  • On start the bot registers the webhook: it reads the public url the platform injects as KEEL_INGRESS_URL and calls setWebhook with <KEEL_INGRESS_URL>/telegram. Without the variable it logs a hint and keeps serving, so the same image also runs locally.

keel ingress hook-bot prints the stable hostname, https://hook-bot-<short org id>.svc.<domain>, the current alias and whether the service is connected (a hostname only answers while the service is up). The dashboard's Service panel shows the same hostname with a copy button.

Rotating the alias

The alias is a second, random label for the same service. If a webhook url leaks, keel ingress hook-bot --rotate-alias (or Rotate alias in the dashboard) replaces it; the old alias stops resolving immediately and the stable hostname is unchanged. Register the webhook again afterwards, which the template does on its next restart: keel service restart hook-bot.

Watching it from everywhere

Surface Where
CLI keel services, keel service status <slug> --follow, keel logs <slug>, keel ingress <slug>
Dashboard /services for every service with live state and uptime; the agent page's Service panel for start, stop, restart, the hostname, the workspace meter and the restart history; /usage for the service hours of the period
MCP keel_list_services, keel_get_service, keel_get_run_logs {agent}, keel_start_service, keel_stop_service, keel_restart_service, keel_get_ingress
API HTTP API, section Services and ingress

Limits worth knowing

  • A plan allows maxServices services running at once (service_limit_reached beyond that); stopped services do not count.
  • Service runs have no deadline: ceilingSeconds is 0 and deadlineAt is null on GET /v1/runs/{id}, and the run detail page shows none, runs until the service stops.
  • keel service stop pauses the agent and keel service start resumes it; an archived agent cannot be started (agent_not_live), and an agent switches between run and service mode only while it has no active run and no running service (mode_change_blocked).
  • Without TELEGRAM_BOT_TOKEN both templates log no token, idle and stay up instead of crash looping; set the secret and keel service restart the bot.