Create a CI/CD Pipeline using Github Actions
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.
Prerequisites
- Public IP of the server (Make sure it's a reserved/elastic IP address so that we can use it on a permanent basis)
- 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:
DOCKER_USERNAME
DOCKER_PASSWORD
PRIVATE_KEY
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 Github.com 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.
name: Deploy App
on:
push:
branches:
- 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.
name: Deploy App
on:
release:
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
jobs:
deploy:
name: Deploy
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.
steps:
- name: Checkout
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.
- name: Push to Docker Hub
uses: docker/build-push-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: <DOCKER_HUB_USERNAME>/<DOCKER_HUB_REPO>
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.
- name: Setup key
id: setup-key
env:
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
run: |
echo "$PRIVATE_KEY" >> $HOME/key.pem
chmod 400 $HOME/key.pem
- name: Update image on server
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)
docker-compose pull
To update the deployment
docker-compose up -d
and finally, to delete unused old containers
docker 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
name: Deploy App
on:
push:
branches:
- master
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Push to Docker Hub
uses: docker/build-push-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: <DOCKER_HUB_USERNAME>/<DOCKER_HUB_REPO>
tags: <IMAGE_TAG>
- name: Setup key
id: setup-key
env:
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
run: |
echo "$PRIVATE_KEY" >> $HOME/key.pem
chmod 400 $HOME/key.pem
- name: Update image on server
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, and I'll get back to you.