How to build and deploy a Telegram bot with Kamal
There are many great tutorials on how to write Telegram bots, but almost none of them cover how to deploy a Telegram bot. In this article, we will write a simple Telegram bot and deploy it with Kamal. It will run in a Docker container exposed via Traefik.
Telegram has been getting more interest from developers all over the world recently. It’s not surprising because it offers an excellent Bot API, which anyone can use to build their own bot for free.
A bot (chatbot) is a program that interacts with users on Telegram. Telegram bots automate tasks such as sending notifications, providing customer service, and more.
The bot that we will write will be a simple echo bot written in Go. Writing advanced Telegram bots is out of scope for this article; I will focus on the deployment part instead.
The bot will be deployed on a simple VPS and exposed to the internet. It will be configured to receive updates via webhooks.
Preparational steps
There are a few important details that we must take care of prior to starting to build and deploy our bot:
-
You must own a domain name so that Let’s Encrypt can issue you a free SSL certificate
We will use Let’s Encrypt because it’s the standard way of deploying Kamal apps. If you don’t own a domain name, you can use your IP address to deploy, but the configuration will be more involved. I will not cover it in this article
-
Telegram bots can get new updates either via long-polling or webhooks. These two methods are completely different
With long-polling, your bot will be asking Telegram servers whether there’s a new update. With webhooks, Telegram will be sending a POST request to your bot. We will use webhooks because this method is more scalable and works perfectly with Kamal
-
I am going to create a subdomain specifically for listening to Telegram webhooks
This works really well from a simplicity standpoint. Only a minimal configuration is needed to make it work. I use Cloudflare to manage my domains. It’s likely that you are the same. In that case, just add your subdomain as an
A
record that points to your VPS that hosts your Telegram bot: -
Because Kamal uses Docker images, you need to use a Docker registry
I use Docker Hub to host the image for the Telegram bot. Make sure to create a new repository. In my case, I called it
kyrylo/telegram-echo-bot
. This is no different from deploying a Rails app with Kamal
Writing a Telegram echo bot
An echo bot is a bot that simply returns (echoes) the input it sees. It’s not really useful in daily life, but it is great for demonstration purposes, since the focus of the article is to show how to deploy any Telegram bot using Kamal.
Let’s initialize a new Go project with go mod init
:
go mod init github.com/kyrylo/telegram-echo-bot
We will need a Telegram Bot API wrapper. There are many libraries to choose from for different programming languages. I am writing the bot in Go, and I prefer github.com/tucnak/telebot.
Let’s import it as our dependency:
go get -u gopkg.in/telebot.v3
Now, let’s create main.go
and write our echo bot (it takes only a few lines of
code):
// main.go
package main
import (
"log"
"os"
tele "gopkg.in/telebot.v3"
)
func main() {
pref := tele.Settings{
Token: os.Getenv("TELEGRAM_BOT_TOKEN"),
Poller: &tele.Webhook{
Listen: "0.0.0.0:3000",
Endpoint: &tele.WebhookEndpoint{
PublicURL: os.Getenv("TELEGRAM_BOT_WEBHOOK_URL"),
},
},
}
b, err := tele.NewBot(pref)
if err != nil {
log.Fatal(err)
return
}
b.Handle(tele.OnText, func(c tele.Context) error {
return c.Send(c.Message().Text)
})
b.Start()
}
This bot simply reads any text that it sees and posts it back at you. There are two environment variables that we need to take care of:
TELEGRAM_BOT_TOKEN
TELEGRAM_BOT_WEBHOOK_URL
TELEGRAM_BOT_TOKEN
TELEGRAM_BOT_TOKEN
is your unique token that @BotFather
gives
you when you create a new bot. It should not be exposed, so keep it safe.
Let’s create a new bot and obtain the token:
- Type
/newbot
into the direct message window with@BotFather
- Specify the bot name:
Echobot
- Specify the bot username: Echo123Bot (it must be unique, so add some random numbers)
- @BotFather returned a token that we need to use in our program:
7473228208:AAFcsA8_g6twHICkBJtnAnWITZNKy26lj1w
Leave the token there for now. We will get back to it later in the article.
TELEGRAM_BOT_WEBHOOK_URL
As I mentioned previously, on a new update (a new event that the bot “saw”),
Telegram will POST it as a webhook. TELEGRAM_BOT_WEBHOOK_URL
controls the
address that Telegram will post to. Your bot listens to this address and
processes the incoming payload. There are many events that could be handled.
Here are some of them:
- a user posted a new message
- someone was invited to the channel
- someone was kicked from the channel
- and many more
TELEGRAM_BOT_WEBHOOK_URL
must be publicly accessible and encrypted via
SSL/TLS. Telegram has a detailed list of requirements that we will
omit in this article. Kamal and Traefik solve most of those headaches.
We will set the value of TELEGRAM_BOT_WEBHOOK_URL
to your subdomain later in
the article. Let’s move on for now.
Building a Docker image
In order to deploy something with Kamal, it needs to be containerized. Let’s package our Telegram bot into a Docker image. Here’s what its Dockerfile will look like:
# Dockerfile
FROM golang:1.22.3-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o ./echo-bot
FROM alpine:3.12 AS run
RUN apk --no-cache add curl
COPY --from=build /app/echo-bot /app/echo-bot
WORKDIR /app
EXPOSE 3000
CMD ["./echo-bot"]
This looks pretty standard. We add curl
for healthchecks and expose port 3000
to listen to. Traefik will forward updates to this port. Healthchecks are
performed on port 3000 as well, so we don’t need to configure anything special
in the Kamal deploy config.
Let’s build it locally and verify that it works:
docker build -t echo-bot:latest .
docker images | grep echo-bot
echo-bot latest 49dc8cfb20ff About a minute ago 15.9MB
Nice! Now we can run it and see it fail:
docker run echo-bot
2024/06/01 08:33:13 telegram: Not Found (404)
The “Not Found” output is from our bot. This is expected, because we didn’t set our environment variables in the previous section.
Setting up Kamal
We are done developing and packaging the bot. Now it’s time to deploy it to production with Kamal.
Install Kamal if you haven’t done so yet:
gem install kamal
Let’s create the Kamal config stubs:
kamal init
Populate config/deploy.yml
with the following content:
# config/deploy.yml
service: telegram-echo-bot
image: kyrylo/telegram-echo-bot
servers:
web:
hosts:
- your-vps-ip
labels:
traefik.http.routers.telegram-echo-bot-web.entrypoints: websecure
traefik.http.routers.telegram-echo-bot-web.rule: Host(`telegram.example.com`)
traefik.http.routers.telegram-echo-bot-web.tls.certresolver: letsencrypt
env:
clear:
TELEGRAM_BOT_WEBHOOK_URL: "https://telegram.example.com/"
secret:
- TELEGRAM_BOT_TOKEN
registry:
username: kyrylo
password:
- KAMAL_REGISTRY_PASSWORD
traefik:
options:
publish:
- "443:443"
volume:
- "/letsencrypt/acme.json:/letsencrypt/acme.json"
args:
accesslog: true
entryPoints.web.address: ":80"
entryPoints.websecure.address: ":443"
entryPoints.web.http.redirections.entryPoint.to: websecure
entryPoints.web.http.redirections.entryPoint.scheme: https
entryPoints.web.http.redirections.entrypoint.permanent: true
certificatesResolvers.letsencrypt.acme.email: "[email protected]"
certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
certificatesResolvers.letsencrypt.acme.httpchallenge: true
certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web
Steps to make this config work for you:
- replace “your-vps-ip” with the actual IP of your VPS
- update the
TELEGRAM_BOT_WEBHOOK_URL
env variable with your URL that you prepared earlier - SSL is configured via Let’s Encrypt (almost every other Kamal tutorial uses this, so no surprises here)
- update
certificatesResolvers.letsencrypt.acme.email
to an email under yourdomain
(example.com won’t work!)
Now, populate .env
with your:
KAMAL_REGISTRY_PASSWORD
- usually, your Docker Hub passwordTELEGRAM_BOT_TOKEN
- copy it from @BotFather
KAMAL_REGISTRY_PASSWORD=your-registry-password
TELEGRAM_BOT_TOKEN=7473228208:AAFcsA8_g6twHICkBJtnAnWITZNKy26lj1w
Note: Kamal generates RAILS_MASTER_KEY
in .env
automatically. You don’t need
that, so simply delete it.
Deploying with Kamal
In order to deploy with Kamal, we need to initialize a Git repository first:
git init
Let’s add .env
to .gitignore
and .dockerignore
so that we don’t leak our
credentials.
echo ".env" | tee -a .gitignore .dockerignore
We are good to commit:
git add .
git commit -m "Initial commit"
SSH into your VPS and add an empty acme.json
file for Let’s Encrypt.
ssh your-server
mkdir /letsencrypt
touch /letsencrypt/acme.json
chmod 600 /letsencrypt/acme.json
You can close your SSH session now. We won’t need it anymore. Use Kamal to provision your VPS with environment variables:
kamal env push
Now we can build and deploy our image. We use setup
here so that Kamal can
install Docker on the VPS, too:
kamal setup
When the setup is done (it may take a couple of minutes), we want to ensure that everything is up and running. Let’s check if the bot is running:
kamal app details
INFO [45c1383f] Running docker ps --filter label=service=telegram-echo-bot --filter label=role=web on your-vps-ip
INFO [45c1383f] Finished in 0.582 seconds with exit status 0 (successful).
App Host: your-vps-ip
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
50e9eed6da21 kyrylo/telegram-echo-bot:2289664e47cbd4927baf3f5d9d0070601cc039bc "./echo-bot" 3 minutes ago Up 3 minutes (healthy) 3000/tcp telegram-echo-bot-web-2289664e47cbd4927baf3f5d9d0070601cc039bc
All good. Now, let’s check if traefik is running:
kamal traefik details
INFO [b97db9aa] Running docker ps --filter name=^traefik$ on your-vps-ip
INFO [b97db9aa] Finished in 0.652 seconds with exit status 0 (successful).
Traefik Host: your-vps-ip
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
296f82ec2374 traefik:v2.10 "/entrypoint.sh --pr…" 3 minutes ago Up 3 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp traefik
Our echo bot is successfully deployed! But does it actually work?
Testing the echo bot
First, we want to verify that the webhook for the bot was set successfully. To
do that, we need to visit
https://api.telegram.org/bot<telegram_bot_token>/getWebhookInfo
.
Substitute your bot token. In my case, the URL was the following: https://api.telegram.org/bot7473228208:AAFcsA8_g6twHICkBJtnAnWITZNKy26lj1w/getWebhookInfo
The response should look like this:
curl -s https://api.telegram.org/bot7473228208:AAFcsA8_g6twHICkBJtnAnWITZNKy26lj1w/getWebhookInfo | jq
{
"ok": true,
"result": {
"url": "https://telegram.example.com/",
"has_custom_certificate": false,
"pending_update_count": 0,
"max_connections": 40,
"ip_address": "104.21.14.82"
}
}
Next, go to Telegram and search for your bot’s username. In my case, it’s
@Echo12485853Bot
.
Now you can press Start
and type something. You will see the bot echoing back
at you your own messages:
Congratulations! You deployed your Telegram bot to a VPS with Kamal 🎉 Yes, it was that easy 😎
Share on: