Automating Telegram Bot Deployment with GitHub Actions and Docker

tjtanjin
9 min readJun 12, 2024

--

Introduction

In previous articles, we explored the benefits of Docker, as well as walked through examples of how we may dockerize our project and integrate CI/CD into our development workflows. However, we haven’t yet examined how to piece them together in practical applications.

In this article, we will embark on a journey to automate the deployment of a Telegram bot with Docker and GitHub Actions. By the end of this walkthrough, you will garner deeper insights into how Docker and GitHub Actions may be used in tandem to streamline the deployment of your projects. So without further ado, let’s dive into the content!

Prerequisites

Before we explore the automating of a Telegram bot deployment, do note that the guide assumes knowledge of the following:

We will not be covering the above as they are not the focus of this guide — links to relevant guides for them have been provided! All that said, if you need help with any of the above, you are more than welcome to reach out for assistance.

Note that the contents of this tutorial can be generalized and applied to projects other than Telegram bots. However, for the purpose of this tutorial, you are encouraged to follow it through with this simple Telegram bot project. Even better if you have read my previous articles, because then you would be comfortable working with Telegram bots by now!

Project Setup

As mentioned, we will be using (TeleQR) as an example so go ahead and clone the project to your local computer with the command:

git clone https://github.com/tjtanjin/tele-qr.git

The project already comes with GitHub Actions setup by default. For the purpose of this tutorial, let us remove that by deleting all the contents within the .github/workflows folder. Once that’s done, we are ready to create our own workflows!

Specifically for TeleQR, we are keen to create 3 workflows:

  • Flake8 Lint (for checking of code styles)
  • Docker Hub (for building and uploading of our docker image)
  • Deployment (for deploying our Telegram bot)

We will create the workflow files in the order above and watch how all of them come together at the end of this! But first, what makes a typical GitHub Actions workflow?

A Typical GitHub Actions Workflow

A GitHub Actions workflow is a YAML file that automates tasks in your development lifecycle, such as for linting your code or deploying your project. Typically, it’s located in the .github/workflows directory of your repository and consist of the following parts:

  1. Name: Provides a convenient identifier for the workflow
  2. Trigger Events: Defines what events trigger the workflow, such as push or pull_request
  3. Jobs and Steps: Specifies jobs that are made up of steps that perform tasks like checking out code, setting up environments, installing dependencies and running tests.
  4. Environment Variables: Defines environment variables that can be used throughout the workflow

There are more properties not covered above but this is enough for understanding the walkthrough. If they look foreign to you, don’t fret! We’ll now curate a simple workflow file for linting our code that will provide you with more clarity.

Create Flake8 Lint Workflow

To create the Lint workflow, let us first create a flake8-lint.yml file within the .github/workflows folder. For linting, we are using flake8 so let us name this workflow Flake8 Lint:

name: Flake8 Lint

We are also keen to run this workflow only when pushes are made to the master branch, so let us include that as a trigger event:

on:
push:
branches: [ "master" ]

Next, we will specify a single lint job to run using the latest Ubuntu image:

jobs:
lint:
name: Lint Codebase
runs-on: ubuntu-latest
steps:
# our steps here

As you can see from the comment in the snippet above, we actually need to define the steps in the job. For this lint job, we will carry out the following 4 steps:

Step 1: Checkout Code

Before we can lint our codebase, we need to understand that workflow runs are done on a runner. In this case, you can think of it as a separate server or virtual machine that will run the linting job. The Checkout Code step uses actions/checkout, which is crucial for bringing your repository code into the runner environment:

- name: Checkout code
uses: actions/checkout@v4

Without this step, your codebase will not even be available for linting!

Step 2: Setup Python

For this Python project, we are using Python 3.10 so let us setup and install this Python version:

- name: Set up Python
run: |
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get update
sudo apt-get install python3.10 -y
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1
sudo update-alternatives --set python3 /usr/bin/python3.10

Step 3: Install Dependencies

Next, in order to run flake8, we need it to be installed:

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8

Step 4: Run flake8

Finally, once the setup is complete, we can do the actual linting by running flake8:

- name: Run Flake8
run: |
python -m flake8

Putting all our configurations together, our final file for linting will look similar to the following:

name: Flake8 Lint
run-name: Flake8 Lint

on:
push:
branches: [ "master" ]

jobs:
lint:
name: Lint Codebase
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
run: |
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get update
sudo apt-get install python3.10 -y
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1
sudo update-alternatives --set python3 /usr/bin/python3.10

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8

- name: Run Flake8
run: |
python -m flake8

Now that we’ve gone through the various parts of the workflow, it’s getting pretty straightforward — isn’t it? Let us move on and look at how we can create the Docker Hub workflow.

Create Docker Hub Workflow

Similar to the Lint workflow, we will add a docker-hub.yml file within the .github/workflows folder. Since we will be publishing a docker image onto Docker Hub in this workflow, let us name it Docker Hub:

name: Docker Hub

However, unlike the Lint workflow, we only want the Docker Hub workflow to run after the Lint workflow is completed. Thus, we add the following trigger event:

on:
workflow_run:
workflows: ["Flake8 Lint"]
types:
- completed

With that said, just waiting for the Lint workflow to be completed before running the Docker Hub workflow is not enough, we also want to ensure that the Lint workflow completed successfully. Thus, we will add a check to our job:

jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
# our steps here

Notice an extra line that checks if the workflow_run concluded successfully. We can then move on to adding the job and steps for our Docker Hub workflow:

Step 1: Checkout Code

As with before, we use actions/checkout to checkout our code:

- name: Checkout code
uses: actions/checkout@v4

Step 2: Login to Docker Hub

In order to publish our Docker Image to Docker Hub, we need to first login to our account. We can make use of docker/login-action for this:

- name: Log in to Docker Hub
uses: docker/login-action@
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

At this point, some of you may be wondering — how are we going to provide the username and password required for logging in? It’s certainly not wise to include our credentials directly in the workflow file, but we can use GitHub secrets!

Head over to your repository settings and look to the tab on the left. Under the Security section, click on Secrets and variables and in the dropdown, click on Actions. This is what you should see:

Since we’ll need a username and password to login to docker hub, we’ll create 2 secrets with the following names:

  • DOCKER_USERNAME
  • DOCKER_PASSWORD

The values for these secrets will have to come from your Docker Hub account. Once these are created, they will be available for our Docker Hub workflow to use!

Step 3: Extract Metadata for Docker

We will next then extract the metadata that will be included with our docker image when we push it to Docker Hub using docker/metadata-action:

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

If you’re observant, you may notice that we have 2 environment variables (REGISTRY and IMAGE_NAME) declared here. Go ahead and declare an env property at the top of the file for these variables in the following manner:

env:
REGISTRY: docker.io
IMAGE_NAME: ${{ github.repository }}

We are basically specifying that the registry we’ll be uploading our docker image to is docker.io and that the repository name will be used as the image name.

Step 4: Build and Push Docker Image

Finally, we build and push our image to Docker Hub:

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

Notice that the tags and labels are obtained from the previous step! Your final Docker Hub workflow file should look similar to the following:

name: Docker Hub
run-name: Docker Hub

on:
workflow_run:
workflows: ["Flake8 Lint"]
types:
- completed

env:
REGISTRY: docker.io
IMAGE_NAME: ${{ github.repository }}

jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Check out the repo
uses: actions/checkout@v3

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

Create Deployment WorkFlow

Upon successfully pushing our docker image to Docker Hub, the final workflow we want to run would be deployment. Create a deployment.yml file within the .github/workflows folder. We will simply name this Deployment:

name: Deployment

For deployment, we only want it to run after our image has been pushed to Docker Hub. Thus, we add the following trigger event:

on:
workflow_run:
workflows: ["Docker Hub"]
types:
- completed

Similarly, we only want to deploy if the Docker Hub workflow succeeded so we add a quick check:

jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
# our steps here

For this workflow, the job only has a single step which calls the deployment script — the script resides on a Virtual Private Server (VPS) that I have:

- name: executing deployment script
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.DEPLOYMENT_HOST }}
username: ${{ secrets.DEPLOYMENT_USERNAME }}
password: ${{ secrets.DEPLOYMENT_PASSWORD }}
script: cd tele-qr/ && ./scripts/deploy.sh tele-qr

Note that we have provided 3 more secrets to authenticate to my VPS using appleboy/ssh-action. For the deployment workflow, it will likely vary depending on how you wish to deploy/host the project. For my project, I hosted it on a simple VPS server and this is the final workflow file:

name: Deployment
run-name: Deployment

on:
workflow_run:
workflows: ["Docker Hub"]
types:
- completed

jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: executing deployment script
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.DEPLOYMENT_HOST }}
username: ${{ secrets.DEPLOYMENT_USERNAME }}
password: ${{ secrets.DEPLOYMENT_PASSWORD }}
script: cd tele-qr/ && ./scripts/deploy.sh tele-qr

Testing & Verification

Having put everything together, we can easily do a quick test to verify that our workflows are setup as intended. Trigger the workflows to run by making any code changes to the project (e.g. adding a comment) and you should see the Lint workflow beginning to run under the Actions tab of your repository. If everything runs successfully, you’ll be greeted with results similar to the following:

Conclusion

Anddddd it’s a wrap! In this tutorial, we’ve gone through detailed steps of setting up our GitHub Actions workflows to automate the deployment of a Telegram bot. If you are keen, you can go further and explore the adding of a test workflow as well as using matrix strategy to run against multiple versions. For reference, the workflows for a chatbot project of mine can be found here.

Finally, I hope that you’ve found this sharing useful, and that you’ll see value in applying what was covered here into your projects. As usual, feel free to share your thoughts, ideas, or feedback in the comments or reach out to me directly. Thank you for your time and see you in the next article!

--

--