Skip to content

🚀 NodeJS CI/CD Pipeline Documentation

GitHub Actions + Harbor + Kubernetes Deployment

This comprehensive lab walks you through establishing a full end-to-end continuous integration and continuous deployment (CI/CD) pipeline for a NodeJS application using GitHub Actions, pushing the resulting image to a private Harbor registry, and deploying it into a self-managed Kubernetes cluster.


📂 Project Repository

  • Repository: git@github.com:amjmaxserve/learn-github-actions.git
  • Application Directory: node-app

The repository structure looks like this:

node-app
├── app.js
├── package.json
├── Dockerfile
├── .github
│   └── workflows
│       └── cicd.yml

🏛️ Architecture

flowchart TD
    A[GitHub Repository] -->|1. Push triggers pipeline| B[GitHub Actions Pipeline]
    B -->|2. Uses| C[Self Hosted Runner]

    C -->|3. Builds| D{Docker Image}
    D -->|4. Pushes image to| E[(Harbor Private Registry)]

    C -->|5. SSH to deploy| F[Production Environment]
    F -.->|6. Pulls image from| E

    F -->|7. Deploys| G(((Application Working)))

🌐 Infrastructure Map

Component Description IP / Domain
DevOps Runner Server GitHub Self Hosted Runner 192.168.29.201
Kubernetes Control Plane Master Node 192.168.29.111
Worker Node Kubernetes Execution Node 192.168.29.3
Container Registry Harbor Container Registry harbor.local
Namespace Kubernetes Namespace target node-app

⚙️ GitHub Self Hosted Runner Installation

Step 1 — Create Runner from GitHub

Navigate to your repository: SettingsActionsRunnersAdd Runner Choose Linux.

Step 2 — Install Runner

Run the following commands on your DevOps server (192.168.29.201):

mkdir actions-runner && cd actions-runner

# Download the latest runner package
curl -o actions-runner-linux-x64.tar.gz -L https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64.tar.gz

# Extract the installer
tar xzf actions-runner-linux-x64.tar.gz

Step 3 — Configure Runner

# Replace XXXXX with the token generated from the GitHub Settings UI
./config.sh --url https://github.com/amjmaxserve/learn-github-actions --token XXXXX

Step 4 — Start Runner

You can run it interactively:

./run.sh

Or install it as a background service:

sudo ./svc.sh install
sudo ./svc.sh start


🔐 GitHub Secrets Configuration

Before creating the pipeline, securely store your critical credentials. Navigate to SettingsSecrets and variablesActions.

Create the following repository secrets:

  • HARBOR_USER: e.g., admin
  • HARBOR_PASSWORD: *******
  • SSH_PRIVATE_KEY: -----BEGIN OPENSSH PRIVATE KEY-----... (Key for master@192.168.29.111)

[!important] Never hardcode these credentials in your pipeline files. They are referenced dynamically using ${{ secrets.SECRET_NAME }}.


🏗️ The Application & Pipeline

Dockerfile

Create a Dockerfile inside node-app:

Dockerfile
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000

CMD ["node","app.js"]

GitHub Actions CI/CD Pipeline

Save this workflow file in .github/workflows/cicd.yml:

.github/workflows/cicd.yml
name: Node App CI/CD

on:
  push:
    branches:
      - master

env:
  REGISTRY: harbor.local
  PROJECT: node-devops
  IMAGE_NAME: node-app
  NAMESPACE: node-app
  CONTROL_PLANE: 192.168.29.111

jobs:
  build-and-deploy:
    runs-on: [self-hosted, Linux, X64]

    steps:
    - uses: actions/checkout@v4

    - uses: actions/setup-node@v4
      with:
        node-version: 20

    - run: npm install

    - run: npm test

    - run: echo "IMAGE_TAG=${GITHUB_SHA}" >> $GITHUB_ENV

    - run: |
        docker build -t $REGISTRY/$PROJECT/$IMAGE_NAME:$IMAGE_TAG .
        docker tag $REGISTRY/$PROJECT/$IMAGE_NAME:$IMAGE_TAG $REGISTRY/$PROJECT/$IMAGE_NAME:latest

    - run: |
        echo "${{ secrets.HARBOR_PASSWORD }}" | docker login harbor.local \
        --username "${{ secrets.HARBOR_USER }}" \
        --password-stdin

    - run: |
        docker push $REGISTRY/$PROJECT/$IMAGE_NAME:$IMAGE_TAG
        docker push $REGISTRY/$PROJECT/$IMAGE_NAME:latest

    - run: |
        mkdir -p ~/.ssh
        echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
        chmod 600 ~/.ssh/id_rsa

    - run: |
        ssh -o StrictHostKeyChecking=no master@$CONTROL_PLANE << 'EOF'
        kubectl rollout restart deployment node-app -n node-app
        kubectl rollout status deployment node-app -n node-app
        EOF

📦 Kubernetes Deployment

You'll need the following manifests applied to the cluster (192.168.29.111) for the initial setup.

Deployment Manifest

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-app
  namespace: node-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: node-app
  template:
    metadata:
      labels:
        app: node-app
    spec:
      imagePullSecrets:
      - name: harbor-secret
      containers:
      - name: node-app
        image: harbor.local/node-devops/node-app:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 3000

Service Manifest

service.yaml
apiVersion: v1
kind: Service
metadata:
  name: node-app
  namespace: node-app
spec:
  type: NodePort
  selector:
    app: node-app
  ports:
  - port: 80
    targetPort: 3000
    nodePort: 30558

🛠️ Troubleshooting History

During this lab setup, you may encounter a few common errors.

[!tip] "1. Harbor login failed" Fix: Check your GitHub secrets. Ensure HARBOR_USER and HARBOR_PASSWORD exactly match your registry credentials.

[!tip] "2. Docker push latest tag failed" Fix: Ensure you run docker tag to associate latest explicitly with the SHA build before pushing.

[!tip] "3. Kubernetes deployment not found" Fix: The SSH step relies on the deployment existing in the namespace. Apply deployment.yaml manually the first time.

[!tip] "4. ImagePullBackOff / 401 Unauthorized" Fix: Create harbor-secret in the node-app namespace with kubectl create secret docker-registry and reference it under imagePullSecrets.

[!tip] "5. SSH access failure" Fix: Add SSH_PRIVATE_KEY secret and potentially bypass standard known_hosts checks using -o StrictHostKeyChecking=no in your pipeline.


✅ Final Output Verification

Check the cluster state after a successful run.

kubectl get pods -n node-app
# Output:
# node-app-6db5678585-bq4wk   Running
# node-app-6db5678585-js5fl   Running

kubectl get svc node-app -n node-app
# Expected: NodePort 30558

kubectl rollout status deployment node-app -n node-app
# Expected: Replicas 2/2 Running

Application Access

Visit the application in your browser: http://192.168.29.3:30558