Create a CI/CD Pipeline using Github Actions

Post Image

In the previous post we saw how to deploy Docker Container(s) on a Virtual Machine using Nginx and Docker Hub, and setup SSL and HTTPS using Certbot. In this post we'll see how we can setup a Github Actions workflow, which will deploy our server every time there's a push on the master/main branch. We'll also see how we can configure the release process, and how we can write different pipelines for different branches.


  1. Public IP of the server (Make sure it's a reserved/elastic IP address so that we can use it on a permanent basis)
  2. Private Key (.pem) file that is used to SSH into the server

Setting up Repository Secrets

Along with Github Actions, Github provides a nice feature called Secrets, in which you can store and encrypt sensitive data on a repository/organization level. We'll utilize this to store our Docker Hub Credentials and the Private Key for server access.

Go to Repository Settings


Look for the Secrets tab


Click on Add Repository Secret


Give your secret a name and a secret value

and click on the Add Secret button to save it


Similarly add your Docker Hub Access Token that we generated in the previous post as DOCKER_PASSWORD and the content of the Private Key as PRIVATE_KEY. At the end, your repository should have these three secrets:


Create a new yml file for our CI/CD workflow

In Github Actions, we've to create a .yml file for each workflow we want to have in our repositories. You can create a workflow on your repository page on directly by going to the Actions tab, and utilize one of the many preconfigured templates by Github according to our use case. But here, we'll create a workflow configuration from scratch just to understand every part of it.

Create a new folder .github in the root directory of your project, and inside it create a workflow directory

Writing the script

Create a new file called deploy.yml (You can name it anything you want)

Now, we want our workflow to trigger on every push on the master branch. So add this part on the top of the yml file.

1name: Deploy App
3 push:
4 branches:
5 - master

But sometimes, we want to have more control on the deployment trigger. For deployments to be triggered manually, we can configure the workflow to run on every release, which has to be created manually.

1name: Deploy App
3 release:
4 types: [created]

Next up, we've to define jobs that the workflow will run. In this case, we only have one job, i.e., deploy

Add this in the deploy.yml file

2 deploy:
3 name: Deploy
4 runs-on: ubuntu-latest

Here, we're giving our job a name of Deploy which will run on an Ubuntu Virtual Instance.

Next up, we'll add steps that our job will perform. Here we've three steps, checkout, build and deploy. The checkout step is necessary as it will pull our code on the Ubuntu Instance. Then, we first build our image and push it to Docker Hub, and the deploy step will SSH into our server and, the latest image from Docker Hub and update our App.

1 steps:
2 - name: Checkout
3 uses: actions/checkout@v2

Indentations matter a lot in a yml file. So make sure that the script is correctly indented according to the final code given at the end of the article.

1 - name: Push to Docker Hub
2 uses: docker/build-push-action@v1
3 with:
4 username: ${{ secrets.DOCKER_USERNAME }}
5 password: ${{ secrets.DOCKER_PASSWORD }}
7 tags: <IMAGE_TAG>

Here, we are using a premade Github Action provide by docker itself, which takes away the hassle of writing the scripts of building and pushing an image. Do note that you can find many such helpful actions on Github Marketplace which you can use in your own workflow.

Also, make sure to replace <DOCKER_HUB_USERNAME> and <DOCKER_HUB_REPO> with your username and repository name on Docker Hub. Replace <IMAGE_TAG> with the tag you want to give to the image. Let's suppose this image is the production image, so you can give it a tag of prod. And, if you're deploying both Backend and Frontend on the same server, you can specify the tag frontend or backend accordingly. Do note that in the previous post we've specified these tags in the docker-compose.yml file.

Next up, we'll write the steps to SSH into the server, pull the latest image and update the server.

1 - name: Setup key
2 id: setup-key
3 env:
4 PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
5 run: |
6 echo "$PRIVATE_KEY" >> $HOME/key.pem
7 chmod 400 $HOME/key.pem
8 - name: Update image on server
9 run: ssh -i $HOME/key.pem -o StrictHostKeyChecking=no ubuntu@<SERVER_IP> 'docker-compose pull && docker-compose up -d && docker image prune -a -f'

Here, in the first step, we're converting the PRIVATE_KEY into a private .pem file for it to be usable in the ssh command.

After that, we're SSHing into the server and giving it three commands to run.

To pull the latest image(s)

1docker-compose pull

To update the deployment

1docker-compose up -d

and finally, to delete unused old containers

1docker image prune -a -f

Make sure to replace <SERVER_IP> with your VM's Public IPv4 IP Address.

Once this final step succeeds, our workflow will be complete, and we'll have our latest code up and running on the server without any hassle.

Our final deploy.yml file should look like this

1name: Deploy App
3 push:
4 branches:
5 - master
8 deploy:
9 name: Deploy
10 runs-on: ubuntu-latest
12 steps:
13 - name: Checkout
14 uses: actions/checkout@v2
16 - name: Push to Docker Hub
17 uses: docker/build-push-action@v1
18 with:
19 username: ${{ secrets.DOCKER_USERNAME }}
20 password: ${{ secrets.DOCKER_PASSWORD }}
22 tags: <IMAGE_TAG>
24 - name: Setup key
25 id: setup-key
26 env:
27 PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
28 run: |
29 echo "$PRIVATE_KEY" >> $HOME/key.pem
30 chmod 400 $HOME/key.pem
31 - name: Update image on server
32 run: ssh -i $HOME/key.pem -o StrictHostKeyChecking=no ubuntu@<SERVER_IP> 'docker-compose pull && docker-compose up -d && docker image prune -a -f'

Once you push the deployment script on the main/master branch of your repo, you can see the Deployment in action in the Actions tab. After the deployment is finished, it should look something like this.


Thank you for reaching at the end of the post. If you liked it, please share it among your network. If you found any errors/discrepenceies, you can contact me any time on my mail or fill the contact form on my Portfolio, and I'll get back to you.