Automating Node.js Deployments With a Custom CI/CD Server
Learn to automate Node.js deployments with a custom CI/CD server using GitHub webhooks, GitHub Actions, PM2, and shell scripting for seamless updates.
Join the DZone community and get the full member experience.
Join For FreeIt is possible that managing and deploying Node.js applications can become a bottleneck as projects grow. Having a properly designed Continuous Integration and Continuous Deployment (CI/CD) pipeline can help reduce the burden of frequent updates, simplify dependency management, and eliminate the need for manual restart processes, thereby avoiding these bottlenecks.
In this tutorial, we will create a custom CI/CD server that listens to GitHub webhook events and performs deployments using GitHub Actions, PM2, and shell scripting. This enables us to:
- Get the latest changes from the GitHub repository when updates are made.
- Identify changes in dependencies and install npm only if there are changes.
- Stop and restart the application using PM2 when there is an update.
- Manage different projects conveniently from a single CI/CD server.
Once you've completed this tutorial, you will have a fully functional self-hosted deployment pipeline that can be used across different projects on AlmaLinux (or any Linux server). This approach is particularly helpful for developers who want more control over their deployment processes than what they can achieve with GitHub Actions or similar third-party CI/CD services.
What We are Building
Our goal is to develop a lightweight Node.js CI/CD server that:
- Listens to the GitHub webhook events based on the commit being pushed to a particular branch.
- Validates the payload to confirm that the request is coming from GitHub.
- Executes a shell script (deploy.sh) that:
- Changes the directory to the right project
- Pulls the repository
- Checks for and installs any missing dependencies
- Restarts the project using PM2
-
Sends the deployment status to GitHub for logging.
This tutorial will walk you through the basics of Node.js, GitHub Actions, and Linux shell scripting. If you are new to PM2 or GitHub webhooks, do not worry, we will explain each in detail.
Let's Set Up Our CI/CD Server
Let's configure a CI\CD server based on Node.js that will automate deployments by monitoring GitHub webhooks and initiating the deployment process using PM2 and shell scripting.
Prerequisites
Make sure you have the following installed on your server before we get started;
1. Node.js (version 16 or newer): You can download it using vim or install it directly from the package manager.
2. PM2: To manage Node.js applications effectively, you can globally install PM2 by following these steps;
npm install -g pm2
3. Git: Required for pulling the latest code. Install via:
sudo dnf install git -y
4. NGINX or Apache (optional): If you want to expose the CI/CD server through a domain.
5. GitHub Setup: Make sure your project is hosted on GitHub and that you have the necessary permissions to configure webhooks.
Creating the CI/CD Server
Step 1: Initialize a Node.js project
First, create a new directory for the CI/CD server and initialize a Node.js project:
mkdir ~/cicd-server && cd ~/cicd-server
npm init -y
Install the required dependencies:
npm install express body-parser crypto
Step 2: Build the webhook listener
Create a file named index.js and populate it with the following code:
const express = require("express");
const bodyParser = require("body-parser");
const crypto = require("crypto");
const { exec } = require("child_process");
const app = express();
const PORT = 4000;
const GITHUB_SECRET = process.env.GITHUB_SECRET || "your-secret-key";
app.use(bodyParser.json());
app.post("/webhook", (req, res) => {
const signature = `sha256=${crypto
.createHmac("sha256", GITHUB_SECRET)
.update(JSON.stringify(req.body))
.digest("hex")}`;
if (req.headers["x-hub-signature-256"] !== signature) {
return res.status(401).json({ message: "Invalid signature" });
}
const repoName = req.body.repository.name;
console.log(`Received update for: ${repoName}`);
exec(`bash ./deploy.sh ${repoName}`, (error, stdout, stderr) => {
if (error) {
console.error(`Deployment failed: ${stderr}`);
return res.status(500).json({ message: "Deployment failed", error: stderr });
}
console.log(`Deployment successful: ${stdout}`);
res.json({ message: "Deployment successful", output: stdout });
});
});
app.listen(PORT, () => console.log(`CI/CD server running on port ${PORT}`));
Step 3: Write the deployment script
Now create a deploy.sh file in the same directory:
REPO_NAME=$1
BASE_DIR="/var/www/projects"
PROJECT_DIR="$BASE_DIR/$REPO_NAME"
echo "Starting deployment for $REPO_NAME..."
if [ ! -d "$PROJECT_DIR" ]; then
echo "Error: Directory $PROJECT_DIR does not exist."
exit 1
fi
cd "$PROJECT_DIR"
echo "Pulling latest changes..."
git pull origin main
CHANGES=$(git diff --name-only HEAD@{1} HEAD)
if [[ $CHANGES == *"package.json"* ]]; then
echo "Detected dependency changes. Running npm install..."
npm install
fi
echo "Restarting application..."
pm2 restart $REPO_NAME
echo "Deployment completed."
Make it executable:
chmod +x deploy.sh
3. Configure GitHub webhooks
- Go to GitHub → Your Repository → Settings → Webhooks.
- Click "Add webhook".
- Set Payload URL to: http://server-ip:4000/webhook
(If you are using NGINX or Apache, replace it with your domain. as in example.com) - Set Content type to "application/json".
- Add a secret with the same value used for
GITHUB_SECRETin yourindex.jsfile. - Choose the push event and click "Add webhook".
4. Run the CI/CD server
Start the CI/CD server with PM2:
pm2 start index.js --name cicd-server
pm2 save
5. Add NGINX or Apache Reverse Proxy
If you want to expose the CI/CD server through a domain, set up an NGINX or Apache reverse proxy.
NGINX configuration
Create an NGINX config file:
sudo nano /etc/nginx/conf.d/cicd.conf
content:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Reload NGINX:
sudo nginx -t
sudo systemctl reload nginx
Apache configuration
Create a virtual host config:
sudo nano /etc/httpd/conf.d/cicd.conf
content:
<VirtualHost *:80>
ServerName example.com
ProxyPass / http://localhost:4000/
ProxyPassReverse / http://localhost:4000/
ErrorLog /var/log/httpd/cicd-error.log
CustomLog /var/log/httpd/cicd-access.log combined
</VirtualHost>
Restart Apache:sudo systemctl restart httpd
6. Test the deployment
Push an update to your repository and check if:
- The webhook triggers the CI/CD server.
- The script pulls changes and installs dependencies if needed.
- PM2 restarts the application.
Wrapping Up
We have successfully created a self-hosted CI/CD pipeline for Node.js deployments using GitHub Actions, PM2, and shell scripting. This solution is lightweight, cost-effective, and can be easily scaled up to accommodate multiple projects.
Opinions expressed by DZone contributors are their own.
Comments