SEO Content Optimizer Tool


This project is a Node JS based SEO Content Optimizer Tool designed to analyze and enhance web content for better visibility on search engines. The tool provides keyword analysis, readability checks, and metadata suggestions to help developers and marketers create high-ranking content.
To streamline development and deployment, we implemented a fully automated CI/CD pipeline using Jenkins. The source code is hosted on GitHub, and every push to the repository triggers an automated workflow that:
Builds and tests the Node.js application using Jenkins.
Packages the app into a Docker image.
Pushes the image to Docker Hub.
Optionally deploys the image to a target server or cloud platform.
GitHub Repository Link: https://github.com/sachindumalshan/node-appp.git
Step 1: Prerequisites
Install Docker (Original)
# Install docker original
sudo apt install docker.io -y
# Verify by running hello-world container
sudo docker run hello-world
Native Jenkins Installation
Instead of Docker-based Jenkins, install Jenkins natively:
sudo apt update && sudo apt upgrade -y
sudo apt install openjdk-17-jdk -y
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee /usr/share/keyrings/jenkins-keyring.asc > /dev/null
sudo apt update
sudo apt install jenkins -y
sudo systemctl start jenkins
sudo systemctl enable jenkins
sudo ufw allow 8080
⚠️ Note: Installing Java JDK 17 separately is not sufficient for the latest Jenkins version. It recommends using Java JDK 21.
Install Git
sudo apt install git
Step 2: Setting Up Jenkins
Access Jenkins from Another Computer
- Open the URL below in your browser. It will display the Jenkins startup interface and prompt you to enter the administrator password. Use the following command to retrieve the password, then enter it in the prompt to log into Jenkins.
# Use 'ifconfig' to get the server IP address
# Ex: http://192.168.8.129:8080/
http://<server-ip>:8080
Get the Password:
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
# If the above command doesn't work, try:
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
If you skip adding user details during setup, you can log in using the default admin account.
Username: admin
Password: Use the password generated using the command shown above.
Then, install the necessary libraries and complete the configurations. You will end up on the Jenkins dashboard.
Install Necessary Plugins
Go to: Manage Jenkins > Manage Plugins
Search and install the plugins as needed. Install essential plugins such as:
Git
Pipeline
NodeJS
Other relevant tools
Configure Build Tools
1. JDK configure
# Name
JDK-17
# Set the JDK path as:
/usr/lib/jvm/java-17-openjdk-amd64
# check java jdk installation path
sudo update-alternatives --config java
If not found, Update and install OpenJDK 17:
sudo apt update
sudo apt install openjdk-17-jdk -y
2. Git configure
# Name
Default
# Set the Git path as:
/usr/bin/git
# To check git path
which git
3. Node JS configure
Name - Node JS-18
✅ check the box Install automatically
Select the Node JS version
4. Docker configure
# Name
Docker
# Set the JDK path as:
/usr/bin/docker
Step 3: Prepare the Application
Create Sample Node.js Application
# Create a new directory for your Node.js application
mkdir my-node-app
# Move into the newly created directory
cd my-node-app
# Initialize a new Node.js project with default settings
# This creates a package.json file with default values
npm init -y
Create index.js
:
import http from "http";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const server = http.createServer((req, res) => {
const filePath = path.join(__dirname, "message.txt");
fs.readFile(filePath, "utf8", (err, data) => {
if (err) {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Error reading file");
} else {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end(data);
}
});
});
server.listen(3000, () => {
console.log("Server is running on port 3000");
});
Create message.txt
:
Hello, this is a Node.js application without Express!
Run:
node index.js
❗ ERROR: To load an ES module, set
"type": "module"
inpackage.json
.
Fix
Add to package.json
:
"type": "module",
Step 4: Push to GitHub
# Initialize Git in the Project Folder
git init
# Create a `.gitignore` File
echo "node_modules/" >> .gitignore
echo ".env" >> .gitignore
# Track Changes and Commit
git add .
git commit -m "Initial commit"
# Connect Local Repository to Remote
git remote add origin https://github.com/sachindumalshan/node-appp.git
# Push Code to the Main Branch
git branch -M main
git push -u origin main
❗ GitHub Push Error: Instead of GitHub password, use Access Token.
Create one from:
https://github.com/settings/tokens → Generate classic token with full access.
Step 5: Jenkins Pipeline Configuration
Create new pipeline in Jenkins:
pipeline {
agent any
// GitHub webhook trigger
triggers {
githubPush()
}
environment {
DOCKER_REGISTRY = 'docker.io'
DOCKER_IMAGE = 'example/node-app'
DOCKER_TAG = "${BUILD_NUMBER}"
DOCKER_CREDENTIALS_ID = 'docker-hub-credentials'
APP_PORT = '5000'
CONTAINER_NAME = 'node-app-container'
}
stages {
stage('Clone Repository') {
steps {
// Use the GitHub token for authentication
checkout scmGit(
branches: [[name: '*/main']],
extensions: [],
userRemoteConfigs: [[
credentialsId: 'githubtoken',
url: 'https://github.com/sachindumalshan/node-appp.git'
]]
)
}
}
stage('Build') {
steps {
sh 'echo "Building the application..."'
sh 'ls -la' // Show what files we have
}
}
stage('Test') {
steps {
sh 'echo "Running tests..."'
}
}
stage('Code Quality') {
steps {
sh 'echo "Running code quality checks..."'
// Add linting, security scanning, etc.
// sh 'npm run lint || echo "No lint script found, skipping..."'
}
}
stage('Docker Build') {
steps {
script {
// Build with both latest and build number tags
echo "Building Docker image: ${DOCKER_IMAGE}:${DOCKER_TAG}"
sh "docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} -t ${DOCKER_IMAGE}:latest ."
}
}
}
stage('Docker Push') {
steps {
script {
sh "docker login -u DockerHub_username -p DockerHub_Password"
sh "docker push ${DOCKER_IMAGE}:${DOCKER_TAG}"
sh "docker push ${DOCKER_IMAGE}:latest"
sh "docker logout"
}
}
}
stage('Pre-Deploy Cleanup') {
steps {
script {
echo "🧹 Starting cleanup process..."
sh '''
set +e # Don't exit on errors - we want to continue cleanup
echo "=== Cleanup Process Started ==="
# Method 1: Direct container removal (most reliable)
echo "1. Removing container: ${CONTAINER_NAME}"
# Check if container exists (running or stopped)
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo " 📦 Container ${CONTAINER_NAME} found, removing..."
# Stop the container first (ignore errors if already stopped)
echo " 🛑 Stopping container..."
sudo docker stop ${CONTAINER_NAME} 2>/dev/null || echo " ℹ️ Container might already be stopped"
# Wait a moment
sleep 2
# Force remove the container
echo " 🗑️ Removing container..."
sudo docker rm -f ${CONTAINER_NAME} 2>/dev/null || echo " ⚠️ Failed to remove container"
# Wait a moment for cleanup
sleep 2
else
echo " ℹ️ No container named ${CONTAINER_NAME} found"
fi
# Method 2: Kill any process using the port
echo "2. Checking port ${APP_PORT} usage..."
# Find process using the port
PORT_PID=$(sudo netstat -tlnp 2>/dev/null | grep ":${APP_PORT} " | awk '{print $7}' | cut -d'/' -f1 | head -1)
if [ -n "$PORT_PID" ] && [ "$PORT_PID" != "-" ] && [ "$PORT_PID" != "" ]; then
echo " ⚠️ Found process $PORT_PID using port ${APP_PORT}"
echo " 💀 Killing process..."
sudo kill -9 "$PORT_PID" 2>/dev/null || echo " ℹ️ Process might already be dead"
sleep 2
else
echo " ✅ Port ${APP_PORT} appears to be free"
fi
# Method 3: Final verification and cleanup
echo "3. Final verification..."
# Double-check container is gone
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo " ⚠️ Container still exists, attempting nuclear removal..."
# Get container ID
CONTAINER_ID=$(docker ps -a --format '{{.ID}}' --filter name=${CONTAINER_NAME})
if [ -n "$CONTAINER_ID" ]; then
echo " 🎯 Container ID: $CONTAINER_ID"
# Try to get and kill the main process
MAIN_PID=$(sudo docker inspect "$CONTAINER_ID" 2>/dev/null | grep '"Pid"' | head -1 | awk -F': ' '{print $2}' | tr -d ',' | tr -d ' ' 2>/dev/null || echo "")
if [ -n "$MAIN_PID" ] && [ "$MAIN_PID" != "0" ] && [ "$MAIN_PID" != "null" ]; then
echo " 💀 Killing main process PID: $MAIN_PID"
sudo kill -9 "$MAIN_PID" 2>/dev/null || echo " ℹ️ PID might already be dead"
sleep 2
fi
# Force remove with container ID
echo " 🗑️ Force removing by ID: $CONTAINER_ID"
sudo docker rm -f "$CONTAINER_ID" 2>/dev/null || echo " ⚠️ Failed to remove by ID"
sleep 2
fi
fi
# Method 4: Clean up any orphaned containers
echo "4. Cleaning up orphaned containers..."
sudo docker container prune -f 2>/dev/null || echo " ℹ️ Container prune completed"
# Final wait
sleep 3
# Verification
echo "\\n=== Cleanup Verification ==="
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo " ❌ ERROR: Container ${CONTAINER_NAME} still exists!"
echo " 📋 Existing containers:"
docker ps -a --format "table {{.Names}}\\t{{.Image}}\\t{{.Status}}" | grep ${CONTAINER_NAME} || true
echo " 🚨 This will cause deployment to fail!"
else
echo " ✅ Container ${CONTAINER_NAME} successfully removed"
fi
# Check port
if sudo netstat -tlnp 2>/dev/null | grep -q ":${APP_PORT} "; then
echo " ⚠️ WARNING: Port ${APP_PORT} still in use"
sudo netstat -tlnp 2>/dev/null | grep ":${APP_PORT} " || true
else
echo " ✅ Port ${APP_PORT} is available"
fi
echo "\\n🎉 Cleanup process completed!"
# Always exit with success to continue pipeline
exit 0
'''
}
}
}
stage('Deploy'){
steps {
script {
echo "🚀 Deploying application..."
// Additional safety check before deployment
sh '''
echo "=== Pre-deployment Safety Check ==="
# Final check if container still exists
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo "❌ FATAL: Container ${CONTAINER_NAME} still exists!"
echo "Attempting emergency removal..."
# Emergency removal
docker stop ${CONTAINER_NAME} 2>/dev/null || true
sleep 2
docker rm -f ${CONTAINER_NAME} 2>/dev/null || true
sleep 2
# Check again
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo "❌ CRITICAL: Unable to remove existing container!"
echo "Manual intervention required."
exit 1
fi
fi
echo "✅ Safety check passed - ready for deployment"
'''
// Wait a moment after cleanup
sleep 2
// Deploy with correct port mapping
sh """
echo "Starting new container: ${CONTAINER_NAME}"
docker run -d \\
--name ${CONTAINER_NAME} \\
--restart unless-stopped \\
-p ${APP_PORT}:${APP_PORT} \\
${DOCKER_IMAGE}:${DOCKER_TAG}
echo "✅ Container started successfully"
"""
// Verify container is running
sh """
echo "=== Deployment Verification ==="
docker ps --format "table {{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Ports}}" | grep ${CONTAINER_NAME} || {
echo "❌ Container not found in running state!"
docker ps -a | grep ${CONTAINER_NAME} || echo "Container not found at all!"
exit 1
}
echo "Container logs (first 20 lines):"
docker logs --tail 20 ${CONTAINER_NAME}
"""
}
}
}
stage('Health Check') {
steps {
script {
echo "🏥 Performing health check..."
sh """
echo "Waiting for application to start..."
sleep 10
echo "Health check attempts:"
SUCCESS=false
for i in 1 2 3 4 5; do
echo "Attempt \$i/5..."
# Check if container is still running
if ! docker ps | grep -q ${CONTAINER_NAME}; then
echo "❌ Container ${CONTAINER_NAME} is not running!"
docker logs ${CONTAINER_NAME}
exit 1
fi
# Check HTTP endpoint
if curl -f -s --max-time 10 http://localhost:${APP_PORT} > /dev/null; then
echo "✅ Health check passed on attempt \$i!"
SUCCESS=true
break
else
echo "❌ Health check failed on attempt \$i"
if [ \$i -lt 5 ]; then
echo "Waiting 10 seconds before retry..."
sleep 10
fi
fi
done
if [ "\$SUCCESS" = "false" ]; then
echo "❌ All health checks failed!"
echo "=== Debug Information ==="
docker ps | grep ${CONTAINER_NAME} || echo "Container not running"
docker logs --tail 50 ${CONTAINER_NAME}
exit 1
fi
echo "🎉 Application is healthy and running!"
"""
}
}
}
}
post {
always {
// Clean up Docker images to save space (but keep recent ones)
sh 'docker image prune -f --filter "until=24h"'
}
success {
echo '✅ Pipeline completed successfully!'
echo "🚀 Application is accessible at: http://100.82.148.95:${APP_PORT}"
// Show deployment summary
sh """
echo "=== Deployment Summary ==="
echo "Image: ${DOCKER_IMAGE}:${DOCKER_TAG}"
echo "Container: ${CONTAINER_NAME}"
echo "Port: ${APP_PORT}"
echo "Build: ${BUILD_NUMBER}"
echo "Time: \$(date)"
echo "Status: \$(docker inspect --format='{{.State.Status}}' ${CONTAINER_NAME})"
echo "=========================="
"""
}
failure {
echo '❌ Pipeline failed!'
// Enhanced debugging information
sh '''
echo "=== Failure Debug Information ==="
echo "Docker containers (all):"
docker ps -a --format "table {{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Ports}}" || true
echo "\\nPort usage:"
netstat -tlnp 2>/dev/null | grep ":5000 " || echo "No process using port 5000"
echo "\\nContainer logs (if exists):"
docker logs --tail 50 ${CONTAINER_NAME} 2>/dev/null || echo "No container logs available"
echo "\\nDocker system info:"
docker system df || true
echo "========================"
'''
}
cleanup {
// Clean workspace
cleanWs()
}
}
}
Then, click the "Build Now" button. This will start the pipeline build process and display the build status.
Step 6: Setting Up GitHub Web hook with Jenkins
Prerequisites
Jenkins must be publicly accessible (not behind a private IP or Tailscale-only address).
Jenkins pipeline is already configured with your GitHub repo.
Webhook endpoint URL (replace with your actual IP or domain):
http://<server-ip>:8080/github-webhook/
💡 If you're using Tailscale, expose Jenkins using Tailscale Funnel:
sudo tailscale funnel 8080
This will give a public URL like:
https://yourname.ts.net/github-webhook/
🪝 Steps to Add GitHub Web hook
Go to your GitHub repository.
Navigate to: Settings → Web-hooks → Add web-hook
In the Payload URL, enter:
http://<your-server-ip>:8080/github-webhook/
(Use your Tail-scale Funnel URL if applicable)
Set Content type to:
application/json
Choose:
- Just the push event
Click Add web hook
Step 7: Update Repository with New Application – SEO Content Optimizer Tool (Gen AI App)
After verifying the Jenkins pipeline works with your basic Node.js app, we now replace it with the actual Gen AI application – a SEO Content Optimizer Tool.
🚀 What's Being Deployed?
This new version of the app uses Generative AI techniques to analyze and optimize content for better SEO performance. It provides:
Keyword extraction
Meta tag suggestions
Readability analysis
Title and description optimization
AI-based content suggestions
📦 Steps to Update the Application Code
Replace Code Locally
Update your project folder (my-node-app/
) with the files for the SEO Optimizer Tool.Make sure it includes your
Dockerfile
, updatedindex.js
, and any other required modules likeopenai
,axios
, etc.Install New Dependencies (if applicable)
npm install
❗Common Errors & Fixes
Below is a list of the key issues encountered during the CI/CD setup and deployment of the SEO Content Optimizer tool, along with the solutions applied.
🔐 1. Jenkins Initial Admin Password Not Found
Problem:
Jenkins prompts for an initial admin password but the default command didn’t work.
Default command:
sudo cat /var/jenkins_home/secrets/initialAdminPassword
✅ Fix: If Jenkins runs as a Docker container, use:
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
☕ 2. JDK Path Not Found for Jenkins Build Tools
Problem:
Unable to configure JDK path under Jenkins build tool settings.
✅ Fix:
sudo apt update
sudo apt install openjdk-17-jdk -y
Set the path in Jenkins as:
/usr/lib/jvm/java-17-openjdk-amd64
📦 3. Node.js App Not Starting
Problem:
Server failed to start with this error:
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module
✅ Fix:
Add the following to your package.json
:
"type": "module",
Then re-run:
node index.js
🔐 4. GitHub Push Authentication Failed
Problem:
While pushing code to GitHub, password authentication failed.
✅ Fix:
Use a Personal Access Token (PAT) instead of your GitHub account password.
Steps to create a token:
Go to GitHub Token Settings
Click Generate new token (classic)
Select required scopes (repo access)
Copy the token and use it as password in the terminal
🐳 5. Docker Commands Failed in Jenkins Pipeline
Problem:
Jenkins pipeline fails during Docker stages with messages like:
docker: command not found
orpermission denied
Root Cause:
Jenkins was using the Snap Docker version while the system used APT Docker, causing conflicts. Jenkins had no permission to access the host Docker daemon.
✅ Fix:
Uninstall both versions:
sudo snap remove docker sudo apt remove docker docker.io
Install Docker cleanly via APT:
sudo apt install docker.io -y
Add Jenkins to Docker group:
sudo usermod -aG docker jenkins sudo systemctl restart jenkins
Verify access:
sudo -u jenkins docker ps
🔄 6. Jenkins Container Doesn’t Persist After Reboot
Problem:
After rebooting the server, the app is no longer accessible.
No container is running.
✅ Fix Options:
- Use the
--restart unless-stopped
flag when starting containers in Jenkins:
docker run -d --name node-app-container --restart unless-stopped -p 5000:5000 <image-name>
- Ensure Docker starts on boot:
sudo systemctl enable docker
- Optionally add a health check stage to re-deploy if the container isn't running.
Subscribe to my newsletter
Read articles from Sachindu Malshan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
