grammY + Docker + GitHub Actions CI/CD + one-click deploy.
Build your bot. Push to deploy.
English | 한국어
Part of Starter Series — Stop explaining CI/CD to your AI every time. Clone and start.
Docker Deploy · Discord Bot · Telegram Bot · Browser Extension · Electron App · npm Package · React Native · VS Code Extension · MCP Server · Python MCP Server · Cloudflare Pages
Via create-starter (recommended):
gh repo create my-telegram-bot --template starter-series/telegram-bot-starter --clone
cd my-telegram-bot && npm install
npm run preflight
cp .env.example .env # fill in BOT_TOKEN from @BotFather
npm run devOr clone directly:
git clone https://github.com/starter-series/telegram-bot-starter my-telegram-bot
cd my-telegram-bot && npm install
npm run preflight
cp .env.example .env
npm run devSee docs/TELEGRAM_SETUP.md for the detailed BotFather guide.
├── src/
│ ├── index.js # Entry point (polling or webhook)
│ ├── config.js # Environment config loader
│ ├── commands/ # Bot commands (auto-loaded)
│ │ ├── start.js # /start — greeting
│ │ └── help.js # /help — list commands
│ ├── handlers/ # Message handlers (auto-loaded)
│ │ └── echo.js # Echo text messages
│ └── lib/
│ ├── health.js # GET /health HTTP server
│ ├── logger.js # Structured JSON logger
│ ├── rate-limiter.js # Per-user rate limiter
│ └── safe-reply.js # Non-throwing reply wrapper (handles Telegram 429 Retry-After)
├── scripts/
│ └── bump-version.js # Bump package.json version
├── tests/
│ ├── commands.test.js # Command + handler tests
│ ├── health.test.js # Health endpoint tests
│ └── safe-reply.test.js # safe-reply 429-retry behavioral tests
├── Dockerfile # Production container
├── docker-compose.yml # Dev with hot reload
├── .github/
│ ├── workflows/
│ │ ├── ci.yml # Audit, lint, test, Docker build
│ │ ├── cd-railway.yml # Deploy to Railway
│ │ ├── cd-fly.yml # Deploy to Fly.io
│ │ └── setup.yml # Auto setup checklist on first use
│ └── PULL_REQUEST_TEMPLATE.md
├── docs/
│ ├── TELEGRAM_SETUP.md # BotFather setup guide
│ └── DEPLOY_GUIDE.md # Railway & Fly.io deployment guide
└── package.json
- grammY — Modern, fast, lightweight Telegram bot framework
- CI Pipeline — Security audit, lint, test, Docker build verification on every push
- CD Pipeline — One-click deploy to Railway or Fly.io + auto GitHub Release
- Docker — Production Dockerfile + dev compose with hot reload
- Polling & Webhook — Long polling by default, webhook mode via env var
- Health endpoint — Built-in
GET /health+ DockerHEALTHCHECKso Fly.io / Railway can detect a crashed bot - Version management —
npm run version:patch/minor/majorto bumppackage.json - Dev mode —
npm run devfor live reload withnode --watch - Starter code —
/start+/helpcommands, echo handler, modular structure - Safe reply —
safe-reply.jsswallows reply errors and obeys Telegram's429 Retry-Afterso a single rate-limited send never crashes the bot loop - Deploy guides — Step-by-step docs for BotFather, Railway, and Fly.io
- Template setup — Auto-creates setup checklist issue on first use
- Supply-chain hardening —
npm ci --ignore-scriptsin CI, committed lockfile, gitleaks pinned by sha256, Railway CLI pinned by version
| Step | What it does |
|---|---|
| Security audit | npm audit for dependency vulnerabilities |
| Lint | ESLint for code quality |
| Build | Syntax smoke check for shipped JavaScript files |
| Test | Jest with coverage thresholds |
| Docker build | Builds the container image to catch build errors |
| Trivy scan | Gates the container image on CRITICAL CVEs |
| Workflow | What it does |
|---|---|
CodeQL (codeql.yml) |
Static analysis for security vulnerabilities (push/PR + weekly) |
Maintenance (maintenance.yml) |
Weekly CI health check — auto-creates issue on failure |
Stale (stale.yml) |
Labels inactive issues/PRs after 30 days, auto-closes after 7 more |
| Step | What it does |
|---|---|
| Version guard | Fails if git tag already exists for this version |
| Deploy | Pushes to Railway or Fly.io |
| GitHub Release | Creates a tagged release with auto-generated notes |
How to deploy:
- Set up GitHub Secrets (see below)
- Bump version:
npm run version:patch(orversion:minor/version:major) - Go to Actions tab → Deploy to Railway (or Fly.io) → Run workflow
| Secret | Description |
|---|---|
RAILWAY_TOKEN |
Railway API token |
RAILWAY_SERVICE_ID |
Target service ID |
See docs/DEPLOY_GUIDE.md for setup guide.
| Secret | Description |
|---|---|
FLY_API_TOKEN |
Fly.io deploy token |
See docs/DEPLOY_GUIDE.md for setup guide.
# Start with hot reload
npm run dev
# Or use Docker
docker compose up
# Token-free preflight: syntax smoke + Jest
npm run preflight
# Bump version (updates package.json)
npm run version:patch # 1.0.0 → 1.0.1
npm run version:minor # 1.0.0 → 1.1.0
npm run version:major # 1.0.0 → 2.0.0
# Lint & test
npm run lint
npm run build
npm testCreate a new file in src/commands/:
// src/commands/echo.js
module.exports = {
name: 'echo',
description: 'Echo your message',
async execute(ctx) {
const text = ctx.match || 'Nothing to echo';
await ctx.reply(text);
},
};Commands are auto-loaded — no need to edit any other file.
Create a new file in src/handlers/:
// src/handlers/photo.js
module.exports = {
name: 'photo',
register(bot) {
bot.on('message:photo', async (ctx) => {
await ctx.reply('Nice photo!');
});
},
};Handlers are auto-loaded — no need to edit any other file.
Security note —
safeReply(ctx, text)defaults to plain text, which is safe to echo. If you switch tosafeReply(ctx, text, { parse_mode: 'HTML' })or'MarkdownV2', escape user input first or an attacker who controls bot input can forge fake links / fake buttons. See grammY formatting docs.
donbarbos/telegram-bot-template (450+ stars) is the most complete Telegram bot template. This template takes a different approach:
| This template | donbarbos/telegram-bot-template | |
|---|---|---|
| Philosophy | Thin starter with CI/CD | Full production stack |
| Framework | grammY (JS) | aiogram (Python) |
| Infrastructure | Docker only | Docker + PostgreSQL + Redis + Grafana |
| Dependencies | 2 runtime | 15+ |
| Learning curve | Read the grammY docs | Learn the full stack |
| CI/CD | Full pipeline included | Full pipeline included |
| AI/vibe-coding | LLMs generate clean JS | LLMs must handle Python + ORM + monitoring |
| Best for | Utility bots, simple automation | Large bots with DB, caching, monitoring |
Choose this template if:
- You want a lightweight bot that's easy to understand and extend
- You need production CI/CD + Docker without the heavyweight stack
- You're using AI tools to generate bot code — simple JS produces the cleanest output
- Your bot doesn't need a database or Redis
Choose donbarbos if:
- You need PostgreSQL, Redis, or monitoring from day one
- You prefer Python/aiogram over JavaScript/grammY
- You want a full production stack out of the box
This template uses JavaScript for simplicity. To add TypeScript:
- Add
typescriptand@types/nodeto devDependencies - Add a
tsconfig.json - Update
npm startto compile and run fromdist/ - Rename
.jsfiles to.ts
TypeScript is opt-in, not forced. For many bots, JavaScript is all you need.
The bot exposes a tiny HTTP health server (src/lib/health.js) so Docker, Fly.io, and Railway can detect a crashed or stuck bot process.
| Mode | Port | Endpoint |
|---|---|---|
| Polling (default) | HEALTH_PORT (default 3000) — standalone server |
GET /health |
Webhook (WEBHOOK_URL set) |
PORT — mounted on the webhook server, no extra listener |
GET /health |
| Status | Body |
|---|---|
200 OK (ready) |
{ "status": "ok", "uptime": <seconds>, "mode": "polling"|"webhook" } |
503 Service Unavailable (starting / disconnected) |
{ "status": "starting", "uptime": <seconds>, "mode": "polling"|"webhook" } |
Configuration
# .env
HEALTH_PORT=3000 # polling mode only — change if 3000 is already taken
# PORT=3000 # webhook mode — /health is mounted on this port
# WEBHOOK_SECRET= # webhook mode only — required when WEBHOOK_URL is setFly.io — add an HTTP service check to fly.toml:
[[http_service]]
internal_port = 3000
force_https = true
[[http_service.checks]]
interval = "30s"
timeout = "5s"
grace_period = "30s"
method = "GET"
path = "/health"Railway — in the service's Settings → Deploy, set the health-check path to /health and the port to 3000.
Docker — docker ps will show (healthy) / (unhealthy) automatically; the HEALTHCHECK runs wget --spider http://localhost:${HEALTH_PORT}/health every 30s.
Currently implemented
- grammY bot with polling (default) and webhook modes,
/start+/helpcommands, echo handler — all auto-loaded fromsrc/commands/andsrc/handlers/. src/lib/safe-reply.js— non-throwing reply wrapper that respects Telegram's429 Retry-After(100% line coverage).src/lib/rate-limiter.js,src/lib/health.js,src/lib/logger.js.- CI:
npm audit --audit-level=high, ESLint, Jest with--coverage, Docker build, Trivy CRITICAL CVE scan, CodeQL (weekly), gitleaks (sha256-pinned). - CD: Railway and Fly.io workflows with a version-tag guard and auto-generated GitHub Releases.
- Test suite: Jest covers bootstrap wiring, env validation, lifecycle, commands, handlers, health, logging, rate limiting, and safe replies (see
npm testoutput).
Planned
- None. A starter is finished when it ships; further features go into downstream projects, not here.
Design intent
- Two runtime dependencies (
grammy,dotenv) and plain JavaScript — so an LLM extending this repo doesn't have to reason about an ORM, a transpile step, or a type system to add a command. safe-replyis non-throwing on purpose: a single rate-limited reply must never crash the polling loop or webhook handler. Errors are logged via the structured logger, not propagated.- Auto-loading via
src/commands/index.jsandsrc/handlers/index.js: drop a file in, the bot picks it up — no central registry to edit, fewer merge conflicts for AI-generated PRs. --ignore-scriptsin CI: lifecycle scripts are a known supply-chain attack surface; the starter opts out by default.- Health endpoint piggybacks on the webhook server when
WEBHOOK_URLis set — one listener, not two.
Non-goals
- TypeScript (opt-in path documented above; not the default).
- Database, ORM, Redis, Grafana, payments, sessions/FSM, i18n — use donbarbos/telegram-bot-template instead (see comparison table above).
- Wrapping the full Telegram Bot API surface — this starter exposes grammY directly.
- A plugin marketplace or theme system; a starter is meant to be forked, not extended.
Redacted
- None. This is a public starter; there are no internal references, customers, or accounts to redact.
PRs welcome. Please use the PR template.