Automating Telegram Bot Deployment with GitHub Actions and Docker
Tags: docker, githubactions, telegrambot, telegram
Published: 2024-06-12 16:16:24

## Introduction

In previous articles, we explored the [benefits of Docker](https://dev.to/tjtanjin/from-screen-to-docker-a-look-at-two-hosting-options-42eh), as well as walked through examples of [how we may dockerize our project](https://dev.to/tjtanjin/how-to-dockerize-a-telegram-bot-a-step-by-step-guide-37ol) and [integrate CI/CD into our development workflows](https://dev.to/tjtanjin/level-up-your-projects-with-github-actions-cicd-2lnh). 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:
- Familiarity with Linux command line
- [Dockerizing a Project](https://dev.to/tjtanjin/how-to-dockerize-a-telegram-bot-a-step-by-step-guide-37ol)
- [Basic Understanding of GitHub Actions](https://dev.to/tjtanjin/level-up-your-projects-with-github-actions-cicd-2lnh)

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](https://discord.com/invite/X8VSdZvBQY) 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](https://github.com/tjtanjin/tele-qr). **Even better** if you have read my [previous articles](https://dev.to/tjtanjin/how-to-build-a-telegram-bot-a-beginners-step-by-step-guide-1kd1), because then you would be comfortable working with Telegram bots by now!

## Project Setup

As mentioned, we will be using ([TeleQR](https://github.com/tjtanjin/tele-qr)) 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:
- **Name:** Provides a convenient identifier for the workflow
- **Trigger Events:** Defines what events trigger the workflow, such as `push` or `pull_request`
- **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.
- **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](https://github.com/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](https://flake8.pycqa.org/en/latest/), 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](https://hub.docker.com/) 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](https://github.com/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](https://github.com/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:

![GitHub Actions Secrets](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4vl0u910d325v09tqscy.png)

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](https://hub.docker.com/) 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](https://github.com/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](https://github.com/tjtanjin/tele-qr/blob/master/scripts/deploy.sh) - 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](https://github.com/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](https://dev.to/tjtanjin/how-to-host-a-telegram-bot-on-ubuntu-a-step-by-step-guide-4jk3) 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:

![GitHub Actions Results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tz20fpyibob7uo9hplul.png)

## 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](https://github.com/tjtanjin/react-chatbotify).

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](https://discord.com/invite/X8VSdZvBQY) to me directly. Thank you for your time and see you in the next article!