🚀 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:
🏛️ 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: Settings → Actions → Runners → Add 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:
Or install it as a background service:
🔐 GitHub Secrets Configuration
Before creating the pipeline, securely store your critical credentials. Navigate to Settings → Secrets and variables → Actions.
Create the following repository secrets:
HARBOR_USER: e.g.,adminHARBOR_PASSWORD:*******SSH_PRIVATE_KEY:-----BEGIN OPENSSH PRIVATE KEY-----...(Key formaster@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:
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:
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
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
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_USERandHARBOR_PASSWORDexactly match your registry credentials.[!tip] "2. Docker push latest tag failed" Fix: Ensure you run
docker tagto associatelatestexplicitly 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.yamlmanually the first time.[!tip] "4. ImagePullBackOff / 401 Unauthorized" Fix: Create
harbor-secretin thenode-appnamespace withkubectl create secret docker-registryand reference it underimagePullSecrets.[!tip] "5. SSH access failure" Fix: Add
SSH_PRIVATE_KEYsecret and potentially bypass standardknown_hostschecks using-o StrictHostKeyChecking=noin 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