Table of Contents
- Introduction
- Prerequisites
- Set Up Git for Version Control
- Build the Node.js Web App
- Testing Setup
- CI/CD with GitHub Actions
- Docker Configuration
- Essential Configuration Files
- Development with Docker Compose
- Test Everything Locally
- Docker Run
- CI/CD with Deployment
- Kubernetes Deployments
- Complete Deployment Workflow
- Hosted on Azure Web App
This project demonstrates a complete end-to-end DevOps workflow by building and deploying a modern Node.js web application. It covers the full lifecycle of application development, from writing and testing code, to containerizing with Docker, automating pipelines with GitHub Actions, and deploying workloads to Kubernetes environments for both staging and production.
As part of this implementation, the application is also hosted on Azure Web App, showcasing how cloud-native platforms can be leveraged for scalable and reliable application delivery. By combining local development with containerization and CI/CD automation, and extending deployments into Kubernetes clusters and Azure services, this project provides a practical demonstration of modern DevOps practices.
Key features of this project include:
Building a Node.js application with testing (Jest, Supertest) and linting (ESLint).
Containerizing the app using Docker and orchestrating services with Docker Compose.
Automating builds, tests, and deployments with GitHub Actions CI/CD pipelines.
Deploying to Kubernetes clusters for staging and production environments.
Hosting the application on Azure Web App to ensure scalability, availability, and cloud integration.
This project serves as a comprehensive learning resource for understanding how software development integrates with DevOps tools and cloud-native deployment strategies.
1. Prerequisites
Install these tools before starting:
Node.js (v18 or higher)
- Download LTS (20.x as of 2025): Node.js
- Verify installation:
node --version # Should show v18.x+ or v20.x+
npm --version # Should show 9.x+ or 10.x+
Git
- Download: Git
- Verify installation:
git --version # Should show 2.34+
Docker Desktop
- Download: Docker Desktop
- Verify installation:
docker --version # Should show 24.x+
docker-compose --version
GitHub Account
- Sign up at: GitHub
Code Editor (Optional)
- Visual Studio Coderecommended
Verify Everything is Installed
node --version # Should show v18.x+ or v20.x+
npm --version # Should show 9.x+ or 10.x+
git --version # Should show 2.34+
docker --version # Should show 24.x+
2. Set Up Git for Version Control
What this step does: Configures Git on your machine so it knows who you are when you make commits, and sets up proper project tracking.
One-time Git Configuration
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
Create and Initialize Project
mkdir nodejs-devops-azure
cd nodejs-devops-azure
git init
3. Build the Node.js Web App
What this step does: Creates a web application using Node.js that can serve web pages and API endpoints
Initialize Project
What this step does: Creates a package.json file that describes your project and manages dependencies.
# Create package.json with default settings
npm init -y
This creates a package.json. Update it as follows:
{
"name": "nodejs-devops-azure",
"version": "1.0.0",
"description": "DevOps learning project with Node.js",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "jest",
"dev": "node app.js",
"lint": "eslint ."
},
"keywords": ["devops", "nodejs", "docker"],
"author": "Your Name",
"license": "MIT",
"engines": { "node": ">=18.0.0" },
"devDependencies": {
"jest": "^29.7.0",
"eslint": "^8.57.0",
"supertest": "^7.1.4"
}
}
click on the package.json and replace the content with the code above.
Create Application File
# Create the main application file
touch app.js
Copy the app.js code into this file.
const http = require('http');
const url = require('url');
const port = process.env.PORT || 3000;
const environment = process.env.NODE_ENV || 'development';
let requestCount = 0;
const startTime = Date.now();
// Enhanced Web Server
const server = http.createServer((req, res) => {
requestCount++;
const timestamp = new Date().toISOString();
const { pathname } = url.parse(req.url, true);
console.log(`${timestamp} - ${req.method} ${pathname} - ${req.headers['user-agent'] || 'Unknown'}`);
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
// Security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
// Route handling
switch (pathname) {
case '/':
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>DevOps Lab 2025</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; }
.endpoint { background: #f8f9fa; padding: 15px; margin: 10px 0; border-radius: 5px; border-left: 4px solid #007bff; }
</style>
</head>
<body>
<div class="header">
<h1> Node.js DevOps Pipeline – Docker, CI/CD, Kubernetes, Azure </h1>
<h2>Built by Izuabueke</h2>
</div>
<p>This project demonstrates a complete end-to-end DevOps workflow by building and deploying a modern **Node.js web application**. It covers the full lifecycle of application development, from writing and testing code, to containerizing with **Docker**, automating pipelines with **GitHub Actions**, and deploying workloads to **Kubernetes** environments for both staging and production.
As part of this implementation, the application is also **hosted on Azure Web App**, showcasing how cloud-native platforms can be leveraged for scalable and reliable application delivery. By combining local development with containerization and CI/CD automation, and extending deployments into Kubernetes clusters and Azure services, this project provides a practical demonstration of modern DevOps practices.</p>
<p>**Key features of this project include:**
- Building a Node.js application with testing (Jest, Supertest) and linting (ESLint).
- Containerizing the app using Docker and orchestrating services with Docker Compose.
- Automating builds, tests, and deployments with GitHub Actions CI/CD pipelines.
- Deploying to Kubernetes clusters for staging and production environments.
- Hosting the application on Azure Web App to ensure scalability, availability, and cloud integration.</p>
<p>This project serves as a comprehensive learning resource for understanding how software development integrates with DevOps tools and cloud-native deployment strategies.</p>
<p>My appreciation goes to my mentor [Raphael Gab-Momoh(MVP)](https://www.linkedin.com/in/rgmh/)[Olalekan OLADIRAN](https://www.linkedin.com/in/oladiranolalekan/)for the time they’ve invested in sharing their knowledge, providing clarity when things seemed complex, and pushing me to keep improving. Your support has made a real difference, and I’m grateful to have learned from such dedicated and inspiring coaches.</p>
<p>Thank you for being part of this journey — I’m excited to keep building on what you’ve taught me.</p>
<h2>Available Endpoints:</h2>
<div class="endpoint">
<strong>GET /</strong> - This welcome page
</div>
<div class="endpoint">
<strong>GET /health</strong> - Health check (JSON)
</div>
<div class="endpoint">
<strong>GET /info</strong> - System information
</div>
<div class="endpoint">
<strong>GET /metrics</strong> - Prometheus metrics
</div>
<p>Environment: <strong>${environment}</strong></p>
<p>Server time: <strong>${timestamp}</strong></p>
<p>Requests served: <strong>${requestCount}</strong></p>
</body>
</html>
`);
break;
case '/health':
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: environment,
version: '1.0.0',
node_version: process.version,
requests_served: requestCount
}, null, 2));
break;
case '/info':
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
platform: process.platform,
architecture: process.arch,
node_version: process.version,
memory_usage: process.memoryUsage(),
environment: environment,
pid: process.pid,
uptime: process.uptime()
}, null, 2));
break;
case '/metrics':
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(`# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total ${requestCount}
# HELP app_uptime_seconds Application uptime in seconds
# TYPE app_uptime_seconds gauge
app_uptime_seconds ${process.uptime()}
# HELP nodejs_memory_usage_bytes Node.js memory usage
# TYPE nodejs_memory_usage_bytes gauge
nodejs_memory_usage_bytes{type="rss"} ${process.memoryUsage().rss}
nodejs_memory_usage_bytes{type="heapUsed"} ${process.memoryUsage().heapUsed}
nodejs_memory_usage_bytes{type="heapTotal"} ${process.memoryUsage().heapTotal}
nodejs_memory_usage_bytes{type="external"} ${process.memoryUsage().external}
`);
break;
default:
res.statusCode = 404;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
error: 'Not Found',
message: `Route ${pathname} not found`,
timestamp: new Date().toISOString()
}, null, 2));
}
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down gracefully');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('Received SIGINT, shutting down gracefully');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});
// Start server
server.listen(port, () => {
console.log(`🚀 Server running at http://localhost:${port}/`);
console.log(`Environment: ${environment}`);
console.log(`Node.js version: ${process.version}`);
});
// Export server for testing
module.exports = server;
Install Dependencies
# Install testing and development tools
npm install --save-dev jest eslint supertest
# Install all dependencies (creates node_modules folder)
npm install
4. Testing Setup
Create Test Folder and File
# Create a folder for your tests
mkdir tests
# Create the main test file
touch tests/app.test.js
Copy the test code into this file.
const request = require('supertest');
const server = require('../app');
describe('App Endpoints', () => {
afterAll(() => {
server.close();
});
test('GET / should return welcome page', async () => {
const response = await request(server).get('/');
expect(response.status).toBe(200);
expect(response.text).toContain('DevOps Lab 2025');
});
test('GET /health should return health status', async () => {
const response = await request(server).get('/health');
expect(response.status).toBe(200);
expect(response.body.status).toBe('healthy');
expect(response.body.timestamp).toBeDefined();
expect(typeof response.body.uptime).toBe('number');
});
test('GET /info should return system info', async () => {
const response = await request(server).get('/info');
expect(response.status).toBe(200);
expect(response.body.platform).toBeDefined();
expect(response.body.node_version).toBeDefined();
});
test('GET /metrics should return prometheus metrics', async () => {
const response = await request(server).get('/metrics');
expect(response.status).toBe(200);
expect(response.text).toContain('http_requests_total');
expect(response.text).toContain('app_uptime_seconds');
});
test('GET /nonexistent should return 404', async () => {
const response = await request(server).get('/nonexistent');
expect(response.status).toBe(404);
expect(response.body.error).toBe('Not Found');
});
});
Create Jest Config
# Create Jest configuration file
touch jest.config.js
Copy the Jest Config code into this file.
module.exports = {
testEnvironment: 'node',
collectCoverage: true,
coverageDirectory: 'coverage',
testMatch: ['**/tests/**/*.test.js'],
verbose: true
};
5. CI/CD with GitHub Actions
Create workflow folder and file:
# Create the GitHub Actions directory structure
mkdir -p .github/workflows
# Create the workflow file
touch .github/workflows/ci.yml
Copy the pipeline code into this file:
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
NODE_VERSION: '20'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npx eslint . --ext .js --ignore-pattern node_modules/
continue-on-error: true
- name: Run tests
run: npm test
- name: Run security audit
run: npm audit --audit-level=moderate || true
build:
name: Build Docker Image
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push'
permissions:
contents: read
packages: write
outputs:
image-digest: ${{ steps.build.outputs.digest }}
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- name: Deploy to staging
run: |
echo "🚀 Deploying to staging environment..."
echo "Image: ${{ needs.build.outputs.image-tag }}"
echo "Would deploy to staging server here"
# In real scenario, you'd use:
# - kubectl apply -f k8s/staging/
# - docker-compose -f docker-compose.staging.yml up -d
# - ssh staging-server "docker pull $IMAGE && docker-compose up -d"
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Deploy to production
run: |
echo "🎯 Deploying to production environment..."
echo "Image: ${{ needs.build.outputs.image-tag }}"
echo "Image digest: ${{ needs.build.outputs.image-digest }}"
echo "Would deploy to production server here"
# In real scenario, you'd use:
# - kubectl apply -f k8s/production/
# - terraform apply
# - ansible-playbook deploy.yml
security-scan:
name: Security Scan
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'ghcr.io/anujulu/my-devops-project:latest' # Note lowercase
format: 'sarif'
output: 'trivy-results.sarif'
env:
TRIVY_USERNAME: ${{ github.actor }}
TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
6. Docker Configuration
Create Dockerfile
# Create the Dockerfile (no extension needed)
touch Dockerfile
Copy the Dockerfile code into this file.
# Fixed and Verified Multi-stage build for optimized image
FROM node:20-alpine AS dependencies
# Update packages for security (Alpine 3.19+ automatically)
RUN apk update && apk upgrade --no-cache
WORKDIR /app
# Copy package files first for better caching
COPY package*.json ./
# Install only production dependencies
RUN npm ci --only=production && npm cache clean --force
# Production stage
FROM node:20-alpine AS production
# Update packages and install necessary tools
RUN apk update && apk upgrade --no-cache && \
apk add --no-cache curl dumb-init && \
rm -rf /var/cache/apk/*
# Create non-root user with proper permissions
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodeuser -u 1001 -G nodejs
WORKDIR /app
# Copy dependencies from previous stage with proper ownership
COPY --from=dependencies --chown=nodeuser:nodejs /app/node_modules ./node_modules
# Copy application code with proper ownership
COPY --chown=nodeuser:nodejs package*.json ./
COPY --chown=nodeuser:nodejs app.js ./
# Switch to non-root user
USER nodeuser
# Expose port
EXPOSE 3000
# Health check with proper timing for Node.js startup
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Use dumb-init for proper signal handling in containers
ENTRYPOINT ["dumb-init", "--"]
# Start application
CMD ["npm", "start"]
Step 7: Essential Configuration Files
What this step does: Creates configuration files that tell various tools what to ignore, how to behave, and what settings to use.
Create .dockerignore
What this file does: Tells Docker which files to ignore when building the container image (similar to .gitignore).
How to create the file:
# Create Docker ignore file
touch .dockerignore
Copy this content into .dockerignore
node_modules
npm-debug
.log*
.git
.github
.env
.env.local
.env.*.local
logs
*.log
coverage
.nyc_output
.vscode
.idea
*.swp
*.swo
.DS_Store
Thumbs.db
README.md
tests/
jest.config.js
.eslintrc*
(coverage .nyc_output .vscode .idea *.swp .swo .DS_Store Thumbs.db README.md tests/ jest.config.js .eslintrc)
Create .gitignore
What this file does: Tells Git which files to ignore and not track in version control (like temporary files, dependencies, etc.).
How to create the file:
touch .gitignore
Copy this content into .gitignore
# Dependencies
node_modules/
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage
coverage/
.nyc_output
# Environment variables
.env
.env.local
.env.*.local
# Logs
logs
*.log
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
(Dependencies node_modules/ npm-debug.log* *.pid .seed .pid.lock coverage/ .nyc_output .env .env.local .env..local logs *.log .vscode/ .idea/ *.swp *.swo .DS_Store Thumbs.db)
Create environment template
What this file does: Shows other developers what environment variables your application needs, without exposing actual secrets.
How to create the file:
# Create environment template file
touch .env.example
Copy this content into .env.example
# Server Configuration
PORT=3000
NODE_ENV=production
# Logging
LOG_LEVEL=info
Create ESLint configuration
What this file does: Configures ESLint to check your JavaScript code for errors and maintain consistent coding style.
How to create the file:
# Create ESLint configuration file
touch .eslintrc.js
module.exports = {
env: {
node: true,
es2021: true,
jest: true
},
extends: ['eslint:recommended'],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module'
},
rules: {
'no-console': 'off',
'no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }]
}
};
8. Development with Docker Compose
What this step does: Creates a Docker Compose file that makes it easy to run your application and any supporting services (like databases) with a single command.
# Create Docker Compose configuration file
touch docker-compose.yml
Copy the docker-compose.yml code into this file.
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- PORT=3000
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
9. Test Everything Locally
Local Run
# Install all dependencies from package.json (including latest versions)
npm install
# Run your test suite to make sure everything works
npm test
# Start the application server
npm start
What you'll see:
Tests should pass with green checkmarks: ✓ GET / should return
welcome page
Server starts and shows: 🚀 Server running at http://localhost:3000/
Test endpoints:
# In a new terminal window, test the endpoints:
curl http://localhost:3000/ # Homepage
curl http://localhost:3000/health # Health check JSON
curl http://localhost:3000/info # System info JSON
curl http://localhost:3000/metrics # Prometheus metrics
10.Docker Commands
# Build image
docker build -t nodejs-devops-azure:latest .
# Run container
docker run -d -p 3000:3000 --name my-devops-container nodejs-devops-azure:latest
# Check container status
docker ps
# Test health check
curl http://localhost:3000/health
# Stop container
docker stop my-devops-container
docker rm my-devops-container
Docker Compose Run
What these commands do: Use Docker Compose to manage multiple services together with a single command.
# Start all services defined in docker-compose.yml
docker-compose up -d
# View real-time logs from all services
docker-compose logs -f
# Stop all services and clean up
docker-compose down
11. CI/CD with Deployment
Initial commit
What this does: Takes a snapshot of all your files and saves it in Git history.
How to commit your code:
# Add all files to Git staging area
git add .
# Create your first commit with a descriptive message
git commit -m "Initial commit: Complete DevOps setup with working CI/CD"
Connect to GitHub
What this step does: Links your local Git repository to a remote GitHub repository and uploads your code.
How to connect and push:
# Set main as the default branch
git branch -M main
# Connect to your GitHub repository (replace yourusername with your actual GitHub username)
git remote add origin https://github.com/yourusername/my-devops-project.git
# Push your code to GitHub for the first time
git push -u origin main
What to expect: Your code will be visible on GitHub, and the CI/CD pipeline will automatically begin executing.
12. Kubernetes Deployments
Create environment folders:
mkdir -p k8s/staging k8s/production
Create Staging Deployment
Create k8s/staging/deployment.yml:
touch k8s/staging/deployment.yml
Copy the staging deployment.yml code into this file.
apiVersion: apps/v1
kind: Deployment
metadata:
name: devops-app-staging
namespace: staging
spec:
replicas: 2
selector:
matchLabels:
app: devops-app
environment: staging
template:
metadata:
labels:
app: devops-app
environment: staging
spec:
containers:
- name: app
image: ghcr.io/Anujulu/nodejs-devops:develop-latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "staging"
- name: PORT
value: "3000"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: devops-app-service-staging
namespace: staging
spec:
selector:
app: devops-app
environment: staging
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
name: devops-app-service-staging
namespace: staging
spec:
selector:
app: devops-app
environment: staging
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: LoadBalancer
Create Production Deployment
Create k8s/production/deployment.yml:
touch k8s/production/deployment.yml
Copy the production deployment.yml code into this file.
apiVersion: apps/v1
kind: Deployment
metadata:
name: devops-app-production
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: devops-app
environment: production
template:
metadata:
labels:
app: devops-app
environment: production
spec:
containers:
- name: app
image: ghcr.io/Anujulu/nodejs-devops:develop-latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3000"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: devops-app-service-production
namespace: production
spec:
selector:
app: devops-app
environment: production
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: LoadBalancer
Step 13: Complete Deployment Workflow
What this step does: Shows you how to use the complete CI/CD pipeline with proper branching strategy for staging and production deployments.
Branch-based Deployment Strategy
- develop branch → Automatically deploys to staging environment
- main branch → Automatically deploys to production environment
- Pull requests → Run tests only (no deployment) Deploy Changes Deploy to staging:
# Create and switch to develop branch
git checkout -b develop
# Make your changes, then commit and push
git add .
git commit -m "Add new feature"
git push origin develop
What happens: GitHub Actions automatically runs tests and deploys to staging.
Deploy to production:
# Switch to main branch
git checkout main
# Merge changes from develop
git merge develop
# Push to trigger production deployment
git push origin main
What happens: GitHub Actions runs full pipeline and deploys to production.
# Check GitHub Actions status
Visit: https://github.com/Anujulu/nodejs-devops-azure/actions
# Check your container registry
Visit: https://github.com/Anujulu/nodejs-devops-azure/pkgs/container/nodejs-devops-azure
# Test your deployed applications (once you have URLs)
curl https://your-staging-url.com/health # Staging health check
curl https://your-production-url.com/health # Production health check
Step 14: Deploy to Azure Web Apps via Azure Portal
After building our Node.js app, containerising it with Docker, and setting up CI/CD with GitHub Actions, we deploy it to Kubernetes, now we’re ready to deploy to Azure Web Apps, a fully managed PaaS that runs our app in the cloud without managing servers or clusters.
App service plan
Step-by-step guide to create an App service plan
On the Azure portal, in the search bar, type App Services plan.
Click on it, then select create.
Basics tab
Fill in the basics:
Subscription – pick your active subscription.
Resource Group – choose or create a new one (Nodejs-app-rg).
Name – must be globally unique (Nodejs-app).
Operating System – Linux.
Region – pick the closest to your users. I will choose UK South.
Plan (Pricing tier):
Free (F1) → dev/test only.
Basic/Standard (B1/S1) → simple production apps.
Premium (P1v3) → better performance, scaling, VNET integration.
Click on Explore pricing plans, change size → pick one (e.g., Standard S1).
for this demo, I will choose S1
Tags
It is the best practice to tag your resource
Name: Project
Value: Nodejs-app
Review + create
App Services
On the Azure portal in the search bar, type “App Services” and click it.
Click + Create → Web App.
Basics tab
Fill in the basics:
Subscription – pick your active subscription.
Resource Group – I will choose the resource group I used for the app service plan (Nodejs-app).
Name – must be globally unique (Nodejs-app).
Publish – I will choose Code because I will deploy Node from my GitHub repo
Runtime stack – choose Node version 22 LTS.
Operating System – Linux.
Region – I will choose my resource group region (UK South).
Pricing plans – It will choose the plan for my app service plan
Deployment settings
Because I want CI/CD integration now:
I will toggle Enable Continuous deployment.
I will choose my organisation, which is my GitHub account
I will connect my repo, which will choose my default branch (main).
Networking: for now, I will use the default Network setting
Monitor and secure
Enable Application Insights
I have Defender for App Service already activated in my subscription
Choose the Same region to avoid extra cost.
Tags
Always remember is the best practice to tag your resource
Name: Project
Value: Nodejs-app
Go to resource
Select Browse or copy the URL to access the application in your browser
Here is the application running in the browser:
Thanks for reading till the end, hope you learn something.
you can put your comment, Like and Share with your friends.
Top comments (0)