SEO Content Optimizer Tool

Sachindu MalshanSachindu Malshan
13 min read

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:

  1. Builds and tests the Node.js application using Jenkins.

  2. Packages the app into a Docker image.

  3. Pushes the image to Docker Hub.

  4. 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" in package.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

  1. Go to your GitHub repository.

  2. Navigate to: SettingsWeb-hooksAdd web-hook

  3. In the Payload URL, enter:

     http://<your-server-ip>:8080/github-webhook/
    

    (Use your Tail-scale Funnel URL if applicable)

  4. Set Content type to:

     application/json
    
  5. Choose:

    • Just the push event
  6. 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

  1. Replace Code Locally
    Update your project folder (my-node-app/) with the files for the SEO Optimizer Tool.

    Make sure it includes your Dockerfile, updated index.js, and any other required modules like openai, axios, etc.

  2. 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
or
permission 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:

  1. Uninstall both versions:

     sudo snap remove docker
     sudo apt remove docker docker.io
    
  2. Install Docker cleanly via APT:

     sudo apt install docker.io -y
    
  3. Add Jenkins to Docker group:

     sudo usermod -aG docker jenkins
     sudo systemctl restart jenkins
    
  4. 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.
0
Subscribe to my newsletter

Read articles from Sachindu Malshan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sachindu Malshan
Sachindu Malshan