CI/CD stands for Continuous Integration and Continuous Delivery (or Deployment). It is a set of practices and tools that automate the process of building, testing, and deploying software — reducing human error and speeding up releases.
| Term | What it Means |
|---|---|
| Continuous Integration (CI) | Automatically build and test code every time a developer pushes a change |
| Continuous Delivery (CD) | Automatically prepare tested code for release to any environment |
| Continuous Deployment | Automatically deploy every passing build straight to production |
| Pipeline | A series of automated steps: build → test → package → deploy |
| Artifact | The output of a build (e.g., a Docker image, JAR file, or ZIP) |
| Stage | A logical group of pipeline steps (e.g., 'Build' or 'Test') |
| Agent/Node | The machine that actually runs the pipeline stages |
| Tool | Role |
|---|---|
| Jenkins | Open-source automation server — the orchestrator of your pipeline |
| Docker | Containers — packages your app + its dependencies into a portable image |
| Azure | Cloud platform — hosts your container registry (ACR) and Kubernetes (AKS) or App Service |
| Git / GitHub | Source code management — every push can trigger a pipeline run |
| Jenkinsfile | A text file (Groovy DSL) checked into Git that defines your pipeline |
| Requirement | Details |
|---|---|
| OS | Windows 10/11, macOS 12+, or Ubuntu 20.04+ |
| RAM | Minimum 8 GB (16 GB recommended when running Jenkins + Docker locally) |
| Disk | At least 20 GB free |
| Internet | Required for downloading images and Azure CLI |
https://www.docker.com/products/docker-desktopdocker --version
# Expected output: Docker version 24.x.x
Enable WSL 2 integration on Windows: Settings → Resources → WSL Integration
# Ubuntu/Debian
sudo apt update && sudo apt install -y openjdk-17-jdk
# macOS (Homebrew)
brew install openjdk@17
# Verify
java -version
# Expected: openjdk version "17.x.x"
# Ubuntu
sudo apt install -y git
# macOS
brew install git
# Windows — Download from https://git-scm.com
# Verify
git --version
# Ubuntu
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# macOS
brew install azure-cli
# Windows — Download MSI from https://aka.ms/installazurecliwindows
# Verify
az --version
# Ubuntu
sudo apt-get install -y kubectl
# macOS
brew install kubectl
# Verify
kubectl version --client
# Login
az login
# List subscriptions
az account list --output table
az account set --subscription ""
# Create a Resource Group
az group create --name cicd-demo-rg --location eastus
ACR is Azure's private Docker registry — like Docker Hub, but inside Azure.
# Create the registry
az acr create \
--resource-group cicd-demo-rg \
--name mycicdregistry \
--sku Basic
# Enable admin access (so Jenkins can push images)
az acr update --name mycicdregistry --admin-enabled true
# Get credentials (save these — you'll need them in Jenkins)
az acr credential show --name mycicdregistry
The easiest way to start Jenkins in any environment is via Docker. This avoids polluting your host OS and makes Jenkins completely portable.
# Create a network and volume
docker network create jenkins
docker volume create jenkins-data
# Run Jenkins (official LTS image with Docker-in-Docker support)
docker run -d \
--name jenkins \
--network jenkins \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins-data:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
jenkins/jenkins:lts-jdk17
# Wait ~30 seconds, then get the initial admin password
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
-v /var/run/docker.sock? This mounts the host Docker socket into Jenkins, allowing Jenkins to run Docker commands (docker build, docker push) inside pipeline stages. Without this, Docker commands inside Jenkinsfile would fail.http://localhost:8080 in your browserGo to: Manage Jenkins → Plugins → Available plugins
| Plugin | Purpose |
|---|---|
| Docker Pipeline | Enables docker.build(), docker.withRegistry() in Jenkinsfile |
| Azure Credentials | Stores Azure SP credentials securely |
| Kubernetes CLI | Runs kubectl commands inside pipeline stages |
| Pipeline: Stage View | Visualises pipeline stages in the Jenkins UI |
| GitHub Integration | Enables webhooks from GitHub to trigger builds |
| Blue Ocean | Modern UI for visualising pipelines (optional) |
| Credentials Binding | Injects secrets as environment variables into pipeline steps |
Credentials are stored securely in Jenkins and referenced by ID in the Jenkinsfile — never hardcode passwords!
acr-credentials ← this exact ID is used in the Jenkinsfile# Create a service principal for Azure deployment
az ad sp create-for-rbac \
--name jenkins-cicd-sp \
--role Contributor \
--scopes /subscriptions/
# This outputs: appId, password, tenant — save all three!
Docker packages your application into a self-contained unit called a container. In a CI/CD pipeline, Docker solves the classic 'it works on my machine' problem — if it runs in a container on your laptop, it runs exactly the same in Azure.
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Stage 2: Production image (smaller — only what's needed)
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
| Instruction | Meaning |
|---|---|
FROM | Base image to start from |
WORKDIR | Set the working directory inside the container |
COPY | Copy files from your local machine into the image |
RUN | Execute a command during image build |
EXPOSE | Documents which port the app listens on |
CMD | The command that runs when a container starts |
# Build an image
docker build -t myapp:1.0 .
# Run a container
docker run -d -p 3000:3000 --name myapp-container myapp:1.0
# View running containers
docker ps
# View logs
docker logs myapp-container
# Tag for ACR
docker tag myapp:1.0 mycicdregistry.azurecr.io/myapp:1.0
# Push to ACR
az acr login --name mycicdregistry
docker push mycicdregistry.azurecr.io/myapp:1.0
| Azure Service | Role in Pipeline |
|---|---|
| Azure Container Registry (ACR) | Private registry for Docker images built by Jenkins |
| Azure Kubernetes Service (AKS) | Managed Kubernetes cluster to run your containers (Option A) |
| Azure App Service | Simpler PaaS option — deploy containers without Kubernetes (Option B) |
# Create cluster (takes 5–10 minutes)
az aks create \
--resource-group cicd-demo-rg \
--name cicd-demo-aks \
--node-count 2 \
--node-vm-size Standard_B2s \
--generate-ssh-keys \
--attach-acr mycicdregistry
# Get credentials so kubectl can talk to your cluster
az aks get-credentials \
--resource-group cicd-demo-rg \
--name cicd-demo-aks
# Verify cluster is running
kubectl get nodes
A Jenkinsfile is a text file written in Groovy DSL, checked into your Git repo. It defines every stage of your pipeline — Jenkins reads it automatically when a build triggers.
pipeline {
agent any
environment {
ACR_NAME = 'mycicdregistry'
ACR_LOGIN_SERVER = "${ACR_NAME}.azurecr.io"
IMAGE_NAME = 'myapp'
IMAGE_TAG = "${BUILD_NUMBER}"
AKS_RG = 'cicd-demo-rg'
AKS_CLUSTER = 'cicd-demo-aks'
}
stages {
stage('Checkout') {
steps {
checkout scm
echo "Building commit: ${env.GIT_COMMIT}"
}
}
stage('Build') {
steps {
sh 'npm ci'
sh 'npm run build'
}
}
stage('Test') {
steps {
sh 'npm run test -- --coverage'
}
post {
always {
junit 'test-results/**/*.xml'
}
}
}
stage('Docker Build & Push') {
steps {
withCredentials([usernamePassword(
credentialsId: 'acr-credentials',
usernameVariable: 'ACR_USER',
passwordVariable: 'ACR_PASS'
)]) {
sh """
docker login ${ACR_LOGIN_SERVER} -u ${ACR_USER} -p ${ACR_PASS}
docker build -t ${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG} .
docker push ${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
}
stage('Deploy to AKS') {
steps {
withCredentials([azureServicePrincipal('azure-service-principal')]) {
sh """
az login --service-principal \
-u ${AZURE_CLIENT_ID} \
-p ${AZURE_CLIENT_SECRET} \
--tenant ${AZURE_TENANT_ID}
az aks get-credentials \
--resource-group ${AKS_RG} \
--name ${AKS_CLUSTER} \
--overwrite-existing
kubectl set image deployment/myapp \
myapp=${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG} \
--record
kubectl rollout status deployment/myapp
"""
}
}
}
}
post {
success {
echo "Pipeline succeeded — Build ${BUILD_NUMBER} deployed to AKS"
}
failure {
echo "Pipeline FAILED — check Stage View for details"
}
}
}
# Create project structure
mkdir cicd-demo && cd cicd-demo
git init
# Minimal Node.js app (index.js)
# const http = require('http');
# const server = http.createServer((req, res) => {
# res.end(`Hello from CI/CD Pipeline! Build: ${process.env.BUILD_NUMBER || 'local'}\n`);
# });
# server.listen(3000);
git add .
git commit -m "Initial commit with Jenkinsfile and Dockerfile"
git push origin main
http://localhost:8080cicd-demo-pipeline → select 'Pipeline'*/main → Script Path: JenkinsfileA webhook tells GitHub to notify Jenkins whenever code is pushed — no manual clicking needed.
http://<JENKINS_PUBLIC_IP>:8080/github-webhook/application/jsonngrok http 8080 to create a public tunnel and use the https://xxx.ngrok.io URL as your webhook payload URL.# Check pods are running
kubectl get pods -n default
# Get the external IP of your service
kubectl get service myapp-service -n default
# Test the app
curl http://
# Expected: Hello from CI/CD Pipeline! Build: 1
az acr credential renew.dockerignore to prevent secrets and node_modules from leaking into imagesdocker scout quickviewnode:20.11-alpine, not node:latest) for reproducibilityparallel block in Jenkinsfile// Parallel stage example
stage('Tests') {
parallel {
stage('Unit Tests') { steps { sh 'npm run test:unit' } }
stage('Integration Tests') { steps { sh 'npm run test:integration' } }
stage('Lint') { steps { sh 'npm run lint' } }
}
}
| Branch | Pipeline Action |
|---|---|
feature/* | Build + test only — no Docker push, no deploy |
develop | Build + test + Docker push to ACR with :dev tag — deploy to dev |
main | Full pipeline — push :latest and :<build> tags — deploy to production |
release/* | Full pipeline — deploy to staging for UAT |
| Error / Symptom | Fix |
|---|---|
docker: command not found in pipeline | Bind mount the host socket and install Docker CLI inside the container, or use a Jenkins agent with Docker pre-installed |
unauthorized: authentication required on docker push | The ACR credential ID in Jenkinsfile doesn't match what you saved in Jenkins Credentials — check spelling exactly |
| Webhook not triggering build | Verify the webhook URL is accessible from the internet (use ngrok locally). Check Jenkins → Manage Jenkins → System Log |
AKS pod stuck in ImagePullBackOff | AKS can't pull from ACR. Ensure you ran az aks update --attach-acr to link them |
kubectl: command not found in Jenkins | Install the Kubernetes CLI plugin in Jenkins, or add kubectl to the Jenkins agent PATH |
Permission denied on /var/run/docker.sock | Add jenkins user to docker group: usermod -aG docker jenkins then restart Jenkins |
# View Jenkins container logs
docker logs jenkins -f
# Shell into Jenkins container for debugging
docker exec -it jenkins bash
# Check ACR images
az acr repository list --name mycicdregistry --output table
az acr repository show-tags --name mycicdregistry --repository myapp
# AKS diagnostics
kubectl describe pod -n default
kubectl logs -n default
kubectl get events -n default --sort-by='.lastTimestamp'
# Rollback a failed deployment
kubectl rollout undo deployment/myapp -n default
docker --version work?