Harnessing the Power of GitHub Actions and Docker for Effortless Next.js CI/CD
Setting Up Automated Build and Deployment Pipelines for Next.js Applications
Working on small projects and deploying to cloud platforms like GCP and AWS or Your Own VPS ?
Implementing CI/CD can be a headache. In this blog, we’ll unravel the secrets of Continuous Integration and Continuous Deployment for Next.js apps. Streamline your development workflow and deploy , with a CI/CD pipeline tailored for the modern web using simple and beginner friendly tools such as GitHub Actions And Docker.
Prerequisites:
- A Virtual Cloud Machine from any cloud provider or a local Ubuntu VPS. In this case, we’ll be using Amazon Web Services (AWS) as our cloud provider.
- Docker, Git, and Docker Compose installed on the given machine.
- A NextJs Project You can check out a template here .
- DockerHub And A GitHub Account
To get started, we’ll launch an AWS EC2 instance and provide a bash script to install the required dependencies. This will ensure a smooth setup for implementing the CI/CD pipeline for our Next.js application.
#!/bin/bash
# install.sh
# to run this file first : chmod +x install.sh
# then you can run it using : ./install.sh
# Update your system
sudo apt update
# Install prerequisites
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Add current user to docker group
sudo usermod -aG docker $USER
# Install Docker Compose
sudo apt install -y docker-compose
# Install Git
sudo apt install -y git
# Clean up
rm get-docker.sh
echo "Installation complete. Please reboot your system for changes to take effect."
5. Add Your Docker Hub Username and Docker Hub Access token into the repository Secrets by navigating into your repository > Settings > Secrets and Variables and add your repository secrets as show in the image below
Understanding the CI/CD Workflow
To better grasp the workings of GitHub Actions, let’s explore a practical example using my Next.Js portfolio website.
1. Trigger:
- The workflow is triggered by a push event to the
main
branch.
2. Build Job (build
):
- This job runs on the
ubuntu-latest
runner. - The workflow first checks out the source code using the
actions/checkout@v4
action. - Then, it builds a Docker image using the
docker build
command. The image is tagged withprabirkalwani/portfolio:<latest>
. - To push the image to Docker Hub, the workflow logs in using the provided Docker username and access token stored in secrets (presumably set up beforehand).
- Finally, the built image is pushed to
prabirkalwani/portfolio:latest
on Docker Hub.
3. Deploy Job (deploy
):
- This job depends on the successful completion of the
build
job. It won't run unless the image is built successfully. - The
deploy
job runs on a self-hosted runner (meaning your own server or infrastructure). - It starts by pulling the latest image (
prabirkalwani/portfolio:latest
) from Docker Hub. - Next, it removes any existing container named
prabir-website
(assuming an old deployment was running). - Finally, the workflow runs the container in detached mode (
-d
) and maps the container port3000
to the host port3000
. The container is namedprabir-website
for easier identification.
Overall Flow:
- You push code to the
main
branch. - The workflow triggers the
build
job which builds and pushes the Docker image to Docker Hub. - Once built, the
deploy
job pulls the image, removes any old container, and starts a new container running your application.
Setting Your CI/CD Pipeline:
Start By Completing the Prerequisites for this Project :
- Connect your GitHub Runner
GitHub Runner offers a self-hosted virtual machine to run workflows - Access your GitHub repository settings
Click on “Actions”
Click on “Runners”
Click on “Add a new self-hosted runner”
Follow the steps and Connect your EC2 instance with GitHub
This will enable running CI/CD processes on the EC2 instance
The EC2 instance will act as the self-hosted runner for your repository
3. Configuring the self hosted runner application as a service
This will configure the self-hosted runner application as a service to automatically start the runner application when the machine starts.Follow the commands below :-
sudo ./svc.sh install
sudo ./svc.sh start
Lets write the code for the action that was explained above in this section :-
To set up the workflow, navigate to the Actions
tab in your repository. Next, click on the New Workflow
button and select the Docker
configuration option.
Then, paste the following code snippet into the workflow editor:
name: Docker-Deploy
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: actions/checkout@v4
- name: Build docker image
run: docker build -t prabirkalwani/portfolio .
- name: Login to Docker Hub
run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_ACCESS_TOKEN }}
- name: Publish image to Docker Hub
run: docker push prabirkalwani/portfolio:latest
deploy:
needs: build
runs-on: self-hosted
steps:
- name: Pull image from Docker Hub
run: sudo docker pull prabirkalwani/portfolio:latest
- name: Delete old container
run: sudo docker rm -f prabir-website
- name: Run Docker container
run: sudo docker run -d -p 3000:3000 --name prabir-website prabirkalwani/portfolio:latest
You can modify the repository name and tag name to suit your project’s requirements. Additionally, you can modify the command is configured to monitor changes on the main branch, but you can alter this branch name as per your preferences.
4.Dockerfile for the NextJs Application
Lastly, we require a Dockerfile to containerize your Next.js application for deployment.
# Docker file for NextJS Application
FROM node:20-alpine3.18 as builder
# Creating an app directory
WORKDIR /app
# copying the package-lock file
COPY package*.json ./
# running npm install here
RUN npm install --production
# copying rest of the files
COPY . .
# creating a build folder
RUN npm run build
# exposing the host port
EXPOSE 3000
# running the application
CMD ["npm", "run", "start"]
Upon committing changes, your continuous integration and continuous deployment (CI/CD) pipeline will be triggered, executing the following workflow steps.
Conclusion
This article is one of many I plan to create on Docker, DevOps, and related technologies that I’m currently exploring and learning. Sharing insights like these is genuinely enjoyable, and I look forward to creating more informative content as I further delve into containerization and DevOps practices.
Thank you for following along, If there are any better alternatives to this approach or suggestions feel free to reach out.
Bonus
Recently, I created a bash script that retrieves system vitals for a VPS, and I found the process immensely enjoyable. I encourage you to take a look at it as well.
#!/bin/bash
#system_info.sh file
# to run this file first : chmod +x system_info.sh
# then you can run it using : ./system_info.sh
# CPU Usage
CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1"%"}')
# Disk Usage
DISK_USAGE=$(df -h | awk '$NF=="/"{printf "Disk Usage: %.2f%\n", $5}')
# Memory Usage
MEMORY_USAGE=$(free | awk '/Mem/{printf("Memory Usage: %.2f%\n"), $3/$2*100}')
# System Uptime
UPTIME=$(uptime -p)
# Load Average
LOAD_AVERAGE=$(uptime | awk -F'average:' '{ print $2 }' | awk '{ print $1, $2, $3 }')
# Display Information
echo "System Information:"
echo "-------------------"
echo "CPU Usage: $CPU_USAGE"
echo "Memory Usage: $MEMORY_USAGE"
echo "Disk Usage: $DISK_USAGE"
echo "Uptime: $UPTIME"
echo "Load Average: $LOAD_AVERAGE"