DEV Community

Cover image for Node.js DevOps Pipeline – Docker, CI/CD, Kubernetes, Azure
Izuabueke Davidson  Anujulu
Izuabueke Davidson Anujulu

Posted on

Node.js DevOps Pipeline – Docker, CI/CD, Kubernetes, Azure

Table of Contents

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+
Enter fullscreen mode Exit fullscreen mode

Git

  • Download: Git
  • Verify installation:
    git --version      # Should show 2.34+ 
Enter fullscreen mode Exit fullscreen mode

Docker Desktop

    docker --version          # Should show 24.x+
    docker-compose --version
Enter fullscreen mode Exit fullscreen mode

GitHub Account

Code Editor (Optional)

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+
Enter fullscreen mode Exit fullscreen mode

Verify Everything is Installed


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
Enter fullscreen mode Exit fullscreen mode

Git Configuration

Create and Initialize Project

  mkdir nodejs-devops-azure
  cd nodejs-devops-azure
  git init
Enter fullscreen mode Exit fullscreen mode

Create and Initialize Project


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
Enter fullscreen mode Exit fullscreen mode

Create package.json with default settings
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"
        }
    }
Enter fullscreen mode Exit fullscreen mode

click on the package.json and replace the content with the code above.
package.json
Create Application File

 # Create the main application file
  touch app.js
Enter fullscreen mode Exit fullscreen mode

main application file
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;
Enter fullscreen mode Exit fullscreen mode

Copy the app.js code into this file

Install Dependencies

  # Install testing and development tools
  npm install --save-dev jest eslint supertest

  # Install all dependencies (creates node_modules folder)
  npm install
Enter fullscreen mode Exit fullscreen mode

Install Dependencies

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
Enter fullscreen mode Exit fullscreen mode

Create Test Folder and File
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');
  });
});
Enter fullscreen mode Exit fullscreen mode

Create Jest Config

  # Create Jest configuration file
  touch jest.config.js
Enter fullscreen mode Exit fullscreen mode

Create Jest Config

Copy the Jest Config code into this file.

  module.exports = {
  testEnvironment: 'node',
  collectCoverage: true,
  coverageDirectory: 'coverage',
  testMatch: ['**/tests/**/*.test.js'],
  verbose: true
    };
Enter fullscreen mode Exit fullscreen mode

Copy the Jest Config code

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
Enter fullscreen mode Exit fullscreen mode

Create workflow folder and file

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 }}
Enter fullscreen mode Exit fullscreen mode

Copy the pipeline code into this file

6. Docker Configuration

Create Dockerfile


  # Create the Dockerfile (no extension needed)
  touch Dockerfile
Enter fullscreen mode Exit fullscreen mode

Create 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"]  
Enter fullscreen mode Exit fullscreen mode

Copy the Dockerfile code into this file

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 
Enter fullscreen mode Exit fullscreen mode

Create Docker ignore file

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*
Enter fullscreen mode Exit fullscreen mode


(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  
Enter fullscreen mode Exit fullscreen mode

Create .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
Enter fullscreen mode Exit fullscreen mode

(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  
Enter fullscreen mode Exit fullscreen mode

Create environment template file
Copy this content into .env.example

# Server Configuration 
PORT=3000 
NODE_ENV=production  
# Logging 
LOG_LEVEL=info
Enter fullscreen mode Exit fullscreen mode

Copy this content into .env.example
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  
Enter fullscreen mode Exit fullscreen mode

Create ESLint configuration file

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': '^_' }]
  }
};
Enter fullscreen mode Exit fullscreen mode

Create ESLint configuration

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
Enter fullscreen mode Exit fullscreen mode

Create Docker Compose configuration file

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
Enter fullscreen mode Exit fullscreen mode

Create Docker Compose configuration file

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
Enter fullscreen mode Exit fullscreen mode

What you'll see:

Tests should pass with green checkmarks: ✓ GET / should return

Test Everything Locally

welcome page
Server starts and shows: 🚀 Server running at http://localhost:3000/

welcome page

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Build image

Stop 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
Enter fullscreen mode Exit fullscreen mode

Start all services defined in docker-compose.yml

View real-time logs from all services

Stop all services and clean up

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"
Enter fullscreen mode Exit fullscreen mode

Create your first commit with a descriptive message

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

Enter fullscreen mode Exit fullscreen mode

GitHub repository and uploads your code

What to expect: Your code will be visible on GitHub, and the CI/CD pipeline will automatically begin executing.

Your code will be visible on GitHub

12. Kubernetes Deployments

Create environment folders:

  mkdir -p k8s/staging k8s/production
Enter fullscreen mode Exit fullscreen mode

Kubernetes Deployments

Create Staging Deployment
Create k8s/staging/deployment.yml:

  touch k8s/staging/deployment.yml
Enter fullscreen mode Exit fullscreen mode

Create Staging Deployment
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
Enter fullscreen mode Exit fullscreen mode

Copy the staging deployment.yml code into this file
Create Production Deployment
Create k8s/production/deployment.yml:

  touch k8s/production/deployment.yml
Enter fullscreen mode Exit fullscreen mode

Create Production Deployment

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
Enter fullscreen mode Exit fullscreen mode

Copy the production deployment.yml code into this file

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 
Enter fullscreen mode Exit fullscreen mode

Branch-based Deployment Strategy

What happens: GitHub Actions automatically runs tests and deploys to staging.

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  
Enter fullscreen mode Exit fullscreen mode

Deploy to production
What happens: GitHub Actions runs full pipeline and deploys to production.

GitHub Actions runs full pipeline and deploys to production.
Monitor Deployments

  # 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 
Enter fullscreen mode Exit fullscreen mode

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.

App service 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

Basics tab

deployment is complete

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

App Services

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).

Deployment settings

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.

Monitor and secure

Tags
Always remember is the best practice to tag your resource
Name: Project
Value: Nodejs-app

Tags
Review + Create

deployment inproress

deployment completed

Go to resource

Select Browse or copy the URL to access the application in your browser

Browse

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)