Dockerize and deploy a NextJS app with Github Actions
Introduction
Deploying a modern web application can be a complex and time-consuming process, but with the right tools and strategies, it can be streamlined and automated. In this article, we'll explore the process of Dockerizing a Next.js application and deploying it using GitHub Actions, a powerful continuous integration and continuous deployment (CI/CD) platform. Next.js is a popular React framework that simplifies the development and deployment of server-rendered React applications. By Dockerizing your Next.js app, you can ensure consistent and reliable deployments across different environments, from development to production. GitHub Actions, on the other hand, provides a flexible and scalable platform for automating your build, test, and deployment workflows, making it an ideal choice for managing the CI/CD process.
Table of Contents
- What is NextJS
- Why Dockerize applications?
- Why? 🤔...
- 1. Unparalleled Portability and Consistency
- 2. Optimized Efficiency and Resource Utilization
- 3. Agile Deployment and Scalability
- 4. Robust Fault Isolation and Security
- 5. Seamless Integration with DevOps Workflows
- DevOps and Continuous Integration and development (CICD)
- Embracing the CI/CD Paradigm
- Hands On Session 🧑🏾💻
- 1. Dockerize the web application
- 2. Docker Compose
- 1. Simplifies multi-container application management.
- 2. Ensures consistent environments:
- 3. Promotes reusability and collaboration:
- 4. Simplifies testing and CI/CD:
- 3. Digital Ocean & Container Registry (DOCR)
- 1. Create a Digital Ocean Account.
- 2. Create a new project.
- 3. Create a Droplet (server) in the new project.
- 4. SSH into the droplet
- 5. Install required packages and configure credentials
- 6. Append CASignatureAlgorithms to sshd_config
- 7. Create a Container registry in Digital Ocean
- 8. Upload local Docker Images to Digital Ocean Container Registry
- 9. Spin containers up in the Digital Ocean Droplet
- 4. Github Actions
- 1. Create a Github Actions workflows
- 2. Push the workflow
- 3. Verify that the pipeline works as expected
- References:
What is NextJS
A framework based on React
If NextJS is an alien term for you, read along!
Next.js provides a powerful set of features and tools for building high-performance, server-rendered React applications. Some of the key benefits and uses of Next.js include: Based on the search results provided, here is a brief overview of Next.js and its key benefits and use cases:
Server-Side Rendering (SSR): Next.js enables server-side rendering, which improves the initial load time of web pages and enhances the overall user experience. This is particularly beneficial for SEO, as search engines can more effectively crawl and index the content.1 2
Automatic Code Splitting: Next.js automatically splits the code into smaller, optimized chunks, improving the initial load time and overall performance of the application. This helps create fast-loading pages. 3
File-Based Routing: Next.js uses a file-based routing system, which simplifies the process of managing application routes and navigation. This makes it easier for developers to build and maintain complex web applications.2
Seamless Error Handling: Next.js comes with built-in error handling capabilities, allowing developers to create custom error pages and provide better user experiences when errors occur. 1
Serverless Functionality: Next.js supports serverless functions, enabling developers to handle backend tasks and API calls without the need for a separate server. This simplifies the deployment and scaling of the application. 2 4
Why Dockerize applications?
Containerization and orchestration compose the key indurstry practices for deploying real-time production-grade applications.
The Docker and Kubernetes platform has emerged as a game-changing technology, revolutionizing the way organizations approach application delivery.
Why? 🤔...
1. Unparalleled Portability and Consistency
Containerization packages your application, dependencies, and runtime environment into a single, portable unit. This ensures your app runs consistently across different environments, from development to production, eliminating the "it works on my machine" problem.
2. Optimized Efficiency and Resource Utilization
Containers are lightweight and share the host operating system, requiring fewer resources than virtual machines. This enables higher server consolidation and utilization, leading to significant cost savings.
3. Agile Deployment and Scalability
Containers can be created and deployed much faster than virtual machines, allowing for more responsive and dynamic application scaling to meet changing demands.
4. Robust Fault Isolation and Security
Each container runs in an isolated environment, limiting the impact of issues or vulnerabilities to the specific container. This improves the overall security and reliability of the application infrastructure.
5. Seamless Integration with DevOps Workflows
Containerization aligns perfectly with modern DevOps practices, enabling automated build, test, and deployment pipelines. This streamlines the development and deployment processes, accelerating software delivery.
DevOps and Continuous Integration and development (CICD)
A cultural and professional movement
The DevOps approach encourages continuous feedback, rapid iteration, and a relentless focus on improving the overall software delivery lifecycle. By aligning the goals and workflows of development and operations, DevOps enables organizations to respond more quickly to changing market demands, reduce the risk of deployment failures, and ultimately deliver better software faster.
Embracing the CI/CD Paradigm
Complementing the DevOps culture, Continuous Integration (CI) and Continuous Deployment (CD) are a set of practices and tools that automate the software development, testing, and deployment processes. CI/CD is all about establishing a rapid feedback loop, where code changes are regularly integrated, tested, and deployed to production with minimal manual intervention.
Hands On Session 🧑🏾💻
This guide assumes that you have a NextJS app built and tested. For this tutorial, I will use a simple NextJS application I have built.
1. Dockerize the web application
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["sh", "-c", "npm run start"]
❗ Now, this is not an optimized Docker Image. In order to build a production-ready Docker image, refer Optimizing Docker Images for Production
Now, for building the image locally, execute the following command:
docker build -t talk-with-data:latest .
and for spinninng a container up:
docker run -it --rm -p 80:3000 talk-with-data:latest
📄 Note the mapping of port 80 on the host machine to port 3000 in the container. This will forward http traffic to the container allowing users to access the application through the host machine's network.
2. Docker Compose
What is Docker Compose? Docker Compose is a tool for defining and running multi-container applications. It allows you to define the services that make up your application in a single YAML configuration file, called a "Compose file". This file specifies how each container in your application should be configured, including the Docker image to use, environment variables, ports, and more.
Why use Docker Compose?
1. Simplifies multi-container application management.
2. Ensures consistent environments:
- The Compose file defines the exact configuration for each service, ensuring that your application will run the same way across different environments (e.g., development, staging, production).
- This helps prevent issues caused by differences in runtime environments.
3. Promotes reusability and collaboration:
- The Compose file acts as a single source of truth for your application, making it easier to share and collaborate on projects.
- Other developers can quickly set up the same application environment by using the Compose file, without needing to manually configure each service.
4. Simplifies testing and CI/CD:
- Docker Compose integrates well with continuous integration and continuous deployment (CI/CD) workflows, allowing you to easily test and deploy your multi-container applications.
- The ability to start and stop the entire application stack with a single command simplifies testing and debugging.
version: '3'
services:
frontend:
image: talk-with-data:latest
ports:
- '80:3000'
Since, we have a single service, the benefits of Docker Compose isn't obvious here, but, when containerizing multiple applications and connecting them to a common network, one can witness the pros of Docker Compose.
3. Digital Ocean & Container Registry (DOCR)
Now, that we have the container up and running locally, let's setup the deployment platform and configure CICD so that changes to the app are automatically deployed to the platform, which in this case, is Digital Ocean.
1. Create a Digital Ocean Account.
2. Create a new project.
3. Create a Droplet (server) in the new project.
Select the Docker OS from Marketplace. Choose a plan type which suits your requirements. I went with the Basic plan.
Configure SSH Key(s):
4. SSH into the droplet
5. Install required packages and configure credentials
For security purposes, Snaps run in complete isolation and need to be granted permission to interact with your system’s resources. Some doctl commands require additional permissions:
Obtain a personal access token:
Create a file .config
in the root directory. Why ? Reference: 5
mkdir ./config
Initialize doctl with the personal access token:
6. Append CASignatureAlgorithms to sshd_config
Execute echo "CASignatureAlgorithms +ssh-rsa" | sudo tee -a /etc/ssh/sshd_config
This is required since we are using a key of type - RSA for accessing the server through SSH. 6
7. Create a Container registry in Digital Ocean
Create a registry to host Docker images. Alternatively, consider using Docker Hub. Digital Ocean, permits 1 free private repository with 500mb storage.
I have created a registry called twd.
8. Upload local Docker Images to Digital Ocean Container Registry
doctl registry login. Ensure that you have doctl installed on your local machine too. 7
execute docker build -t registry.digitalocean.com/twd/talk-with-data:latest .
alternatively, configure a new tag for the existing image
docker tag talk-with-data registry.digitalocean.com/<my-registry>/<my-image>
Push the image to the container registry docker push registry.digitalocean.com/<my-registry>/<my-image>
9. Spin containers up in the Digital Ocean Droplet
Now, that the Docker imagefor the NextJS app is available on Digital Ocean, let's utilize the image to spin a container up.
docker run -it --rm -p 80:3000 registry.digitalocean.com/twd/talk-with-data:latest
Finally let's configure some additional firewall rules so that the application is accessible through the internet. SSH into your droplet and execute the following commands:
sudo ufw status #Check the status of the firewall
sudo ufw enable #enable the firewall if it isn't active
sudo ufw allow http
sudo ufw allow https #Enable HTTP and HTTPS traffic
sudo ufw allow ‘Nginx Full’ #We will also be enabling reverse proxying through Nginx, in the next article.
When you access the Firewall settings through the Digital Ocean droplet's console, the Firewall settings should look like this:
There you go! You should have a docker container running successfully on your Digital Ocean droplet.
4. Github Actions
Well, that was interesting! Now, whenever you make changes to the app, you wouldn't want to manually deploy these changes to the Digital Ocean droplet, or any server for that matter. This is where CICD comes into the picture.
Let's create a CICD workflow with Github actions!
1. Create a Github Actions workflows
Navigate to the .github
directory under your root directory. Create a directory titled Workflows
. In workflows
create a yaml file. I have named mine build-and-deploy.yml
.
name: Continuous Integration and Deployment
# Control when the workflow is triggered
on:
push:
branches: [main, feature/*]
workflow_dispatch:
inputs:
version:
description: 'Docker Image Version'
required: true
#Variables
env:
# REGISTRY: 'registry.digitalocean.com/talk-with-data'
REGISTRY: 'registry.digitalocean.com/twd-registry'
IMAGE_NAME: 'frontend'
DIRECTORY: 'talk-with-data'
TASKWEAVER: 'taskweaver'
#Job Descriptions
jobs:
build-and-push-image:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Build Docker Image
run: docker build -t $(echo $REGISTRY)/$(echo $IMAGE_NAME):$(echo $GITHUB_SHA | head -c7) .
- name: Install Digital Ocean CLI
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.TWD_DIGITALOCEAN_ACESS_TOKEN}}
- name: Login to Digital Ocean Container Registry
run: doctl registry login --expiry-seconds 600
- name: Remove obsolete images
run: if [ ! -z "$(doctl registry repository list | grep "$(echo $IMAGE_NAME)")" ]; then doctl registry repository delete-manifest $(echo $IMAGE_NAME) $(doctl registry repository list-tags $(echo $IMAGE_NAME) | grep -o "sha.*") --force; else echo "No repository"; fi
- name: Push Docker Image to Digital Ocean Container Registry
run: docker push $(echo $REGISTRY)/$(echo $IMAGE_NAME):$(echo $GITHUB_SHA | head -c7)
deploy:
runs-on: ubuntu-latest
needs: build-and-push-image
steps:
- name: Deploy to Digital Ocean Droplet via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{secrets.TWD_HOST}}
username: ${{secrets.USERNAME}}
key: ${{secrets.SSH_KEY}}
envs: IMAGE_NAME, REGISTRY, GITHUB_SHA, DIRECTORY, TASKWEAVER, {{secrets.TWD_DIGITALOCEAN_ACESS_TOKEN}}
script: |
echo "Current user: $(whoami)"
cd "${DIRECTORY}"
echo "Current directory: $(pwd)"
docker login -u ${{secrets.TWD_DIGITALOCEAN_ACESS_TOKEN}} -p ${{secrets.TWD_DIGITALOCEAN_ACESS_TOKEN}} registry.digitalocean.com
echo "Terminating existing Docker containers..."
docker compose down --remove-orphans
docker stop $(docker ps -q)
docker rm -f $(docker ps -aq)
doctl auth init --access-token ${{secrets.TWD_DIGITALOCEAN_ACESS_TOKEN}}
export TAG_TASKWEAVER=$(doctl registry repository list-tags $(echo ${TASKWEAVER}) --format Tag | tail -n +2)
export TAG_FRONTEND=$(echo ${GITHUB_SHA} | head -c7)
echo "::set-output name=Tag-Frontend::$TAG_FRONTEND"
echo "::set-output name=Tag-Taskweaver::$TAG_TASKWEAVER"
# docker run -d --name $(echo $IMAGE_NAME)-$(echo $TAG_FRONTEND) -p 3000:3000 $(echo $REGISTRY)/$(echo $IMAGE_NAME):$(echo $TAG_FRONTEND)
# docker run -d --name $(echo $TASKWEAVER)-$(echo $TAG_TASKWEAVER) -p 8000:8000 $(echo $REGISTRY)/$(echo $TASKWEAVER):$(echo $TAG_TASKWEAVER)
docker compose up -d
docker container prune -f
Let's also create the required Secrets on Github. Navigate to your Github repository. Then navigate to Settings -> Secrets & Variables -> Actions.
Configure variables for:
- Host (Droplet's public Ipv4 Address)
- Username (Usually
root
) - Digital Ocean Personal Access token
- SSH Private Key obtained whilst creating a SSH key for the droplet. Ensure that you copy the entire key, including the "begin" and "end" parts.
2. Push the workflow
3. Verify that the pipeline works as expected
References:
Footnotes
https://www.intuz.com/blog/5-reasons-why-you-should-use-next.js-for-your-front-end-development ↩ ↩2
https://ralabs.org/blog/why-to-use-next-js-6-key-benefits/ ↩ ↩2 ↩3
https://dev.to/richkurtzman/advantages-and-disadvantages-of-nextjs-5hg6 ↩
https://docs.digitalocean.com/reference/doctl/how-to/install/ ↩