DEV Community

Cover image for From Code to Cloud: Seamless CI/CD with GitHub Actions and Azure Web Apps
Ibrahim Bio Abubakar
Ibrahim Bio Abubakar

Posted on

From Code to Cloud: Seamless CI/CD with GitHub Actions and Azure Web Apps

Have you ever built a cool Node.js app on your local machine and thought, “Now what? How do I get this running in the cloud without manually pushing code every time?” That’s exactly where CI/CD (Continuous Integration and Continuous Deployment) comes to the rescue.

In this article, I’ll walk you through how I took a simple Node.js application from code to cloud using GitHub Actions, Docker, Kubernetes, and Azure Web App Services. We’ll set up a seamless pipeline that automatically builds, tests, and deploys changes the moment you push to GitHub—no more manual deployments, no more “it works on my machine” headaches.

By the end, you’ll see how easy it is to:

  • Containerize your Node.js app with Docker 🐳
  • Orchestrate deployments with Kubernetes ⚙️
  • Automate CI/CD using GitHub Actions 🤖
  • Deploy effortlessly to Azure Web Apps ☁️

Let’s dive in and turn your local project into a production-ready cloud app!


Prerequisites - Download These First!

Before starting, you need to install these tools on your machine:

  • Node.js (v18 or higher)Download (choose LTS version 20.x)
   node --version
   npm --version
Enter fullscreen mode Exit fullscreen mode
   git --version
Enter fullscreen mode Exit fullscreen mode
   docker --version
   docker-compose --version
Enter fullscreen mode Exit fullscreen mode

Image 1

  • Code Editor (optional, recommended)VS Code

Step 1: Set Up Git for Version Control

Configure Git:

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

Output of command in VScode terminal:

Image 2

Initialize your project:

mkdir my-devops-project
cd my-devops-project
git init
Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:

Image 3


Step 2: Build a Node.js Web App

Initialize project:

# Create package.json with default settings
npm init -y
Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:
Image 4

Update package.json with scripts, metadata, and dev tools (Jest, ESLint, Supertest).

{
  "name": "my-devops-project",
  "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

Then create app.js with routes for /, /health, /info, /metrics, and graceful shutdown logic.

# Creates file

touch app.js

Enter fullscreen mode Exit fullscreen mode

Copy and paste this into app.js file:

// core modules
const http = require("http");
const url = require("url");

// environment configuration
const PORT = process.env.PORT || 3000;
const ENVIRONMENT = process.env.NODE_ENV || "development";

let requestCount = 0;

// helper: send JSON responses
function sendJSON(res, statusCode, data) {
  res.statusCode = statusCode;
  res.setHeader("Content-Type", "application/json");
  res.end(JSON.stringify(data, null, 2));
}

// helper: send HTML responses
function sendHTML(res, statusCode, content) {
  res.statusCode = statusCode;
  res.setHeader("Content-Type", "text/html");
  res.end(content);
}

// helper: send Prometheus metrics
function sendMetrics(res) {
  const mem = process.memoryUsage();
  const metrics = `
# 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"} ${mem.rss}
nodejs_memory_usage_bytes{type="heapUsed"} ${mem.heapUsed}
nodejs_memory_usage_bytes{type="heapTotal"} ${mem.heapTotal}
nodejs_memory_usage_bytes{type="external"} ${mem.external}
`;
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/plain");
  res.end(metrics);
}

// main server
const server = http.createServer((req, res) => {
  requestCount++;
  const timestamp = new Date().toISOString();
  const { pathname } = url.parse(req.url, true);

  // logging
  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 "/":
      sendHTML(
        res,
        200,
        `
<!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>Fullstack DevOps Lab 2025</h1>
    <p>Modern Node.js application with CI/CD pipeline</p>
    <p>Version: 1.0.0</p>
  </div>
  <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":
      sendJSON(res, 200, {
        status: "healthy",
        timestamp,
        uptime: process.uptime(),
        environment: ENVIRONMENT,
        version: "1.0.0",
        node_version: process.version,
        requests_served: requestCount,
      });
      break;

    case "/info":
      sendJSON(res, 200, {
        platform: process.platform,
        architecture: process.arch,
        node_version: process.version,
        memory_usage: process.memoryUsage(),
        environment: ENVIRONMENT,
        pid: process.pid,
        uptime: process.uptime(),
      });
      break;

    case "/metrics":
      sendMetrics(res);
      break;

    default:
      sendJSON(res, 404, {
        error: "Not Found",
        message: `Route ${pathname} not found`,
        timestamp,
      });
  }
});

// graceful shutdown
function shutdown(signal) {
  console.log(`\nReceived ${signal}, shutting down gracefully...`);
  server.close(() => {
    console.log("Server closed");
    process.exit(0);
  });
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));

// 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 for testing
module.exports = server;

Enter fullscreen mode Exit fullscreen mode

Install dependencies:

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

# Install all dependencies

npm install
Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:

Image 6

Output of command in VScode terminal:

Image 7


Step 3: Create Proper Tests

We are going to setup an automated testing that verify the application works correctly everytime a change is made.

Create a test directory and a test file tests/app.test.js

# Create a folder for your tests
mkdir tests

# Create the main test file
touch tests/app.test.js

Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:

Image 8

Copy and paste the code below into tests/app.test.js:


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 Configuration

Jest is a JavaScript testing framework developed by Facebook (now Meta). It’s widely used in Node.js and React projects for testing code.

Create a jest.config.js file in the root folder for configuration.


touch jest.config.js

Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:

Image 9

Copy and paste the code below in the jest.config.js file


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

Enter fullscreen mode Exit fullscreen mode

Step 4: GitHub Actions CI/CD Pipeline

This creates an automated pipeline that runs tests and builds Docker images every time you push code to GitHub.

Create .github/workflows/ci.yml to:

  • Run tests and linting
  • Build & push Docker images to GitHub Container Registry
  • Run vulnerability scans
  • Deploy to staging/production based on branch
# Create the GitHub Actions directory structure
mkdir -p .github/workflows

# Create a ci.yml in .github/workflows directory
touch .github/workflows/ci.yml

Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:

Image 10

name: CI 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/YOU_GITHUB_USERNAME/my-devops-project:latest'  #All in lower case.  
          format: 'sarif'
          output: 'trivy-results.sarif'
    # env:
    #   TRIVY_USERNAME: ${{ github.actor }}
    #   TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

Note: You'll need to replace YOU_GITHUB_USERNAME with your GitHub username.


Step 5: Dockerfile

This creates instructions for Docker to build a container image of your application that can run on any machine.

Create a multi-stage Dockerfile that:

  • Installs dependencies
  • Uses non-root user for security
  • Adds health checks
  • Exposes port 3000
#Create a Dockerfile

touch Dockerfile

Enter fullscreen mode Exit fullscreen mode

Copy and paste the codes below into Dockerfile.

# Multi-stage build for optimized image
FROM node:20-alpine AS dependencies

# Update packages for security
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

Step 6: Essential Configuration Files

Lets creates configuration files that tell various tools what to ignore, how to behave, and what settings to use.

Create the following:

  • .dockerignore
  • .gitignore
  • .env.example
  • .eslintrc.js

Output of command in VScode terminal:

Image 11

Create .dockerignore, copy and paste the code below:


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

Create .gitignore, copy and paste the code below:

# 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

Create .env.example, copy and paste the code below:


# Server 
Configuration PORT=3000 
NODE_ENV=production  
# Logging 
LOG_LEVEL=info

Enter fullscreen mode Exit fullscreen mode

Create .eslintrc.js, copy and paste the code below:


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

Step 7: Docker Compose for Development

Now let's create a Docker Compose file that makes it easy to run your application and any supporting services with a single command.

# Create a docker-compose.yml file 

touch docker-compose.yml

Enter fullscreen mode Exit fullscreen mode

Create docker-compose.yml to run the app with, copy and paste the code below:


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

Step 8: Test Everything Locally

This demonstrates how to run and test your application locally before deploying it.

Run locally:

# Run your test suite to make sure everything works
npm test

Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:

Image 14a

You should see all 5 tests pass successfully:

Image 14b

This starts the app in your local environment:

# Start the application server
npm start
Enter fullscreen mode Exit fullscreen mode

What you'll see:

Image 15a

Your browser should give this output:

Image 15b

Test endpoints (using terminal windows)


---

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

Docker Commands:

Before running the Docker commands, ensure you've launched your Docker Desktop application.

This command builds the Docker image:


# Build image
docker build -t my-devops-app:latest .

Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:
Image 16a

This shows the newly built container image my-devops-app:latest in Docker Desktop:

Image 45

# Run container

docker run  --name my-devops-container -d -p 3000:3000 --restart unless-stopped my-devops-app:latest

Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:

Image 16b

This shows the running container (my-devops-container) on Docker Desktop:

Image 46

From your browser (http://localhost:3000), you should see this:

Image 17

# Check container status
docker ps

# Test health check in terminal
curl http://localhost:3000/health

Enter fullscreen mode Exit fullscreen mode
# Stop container

docker stop my-devops-container

# Delete Container
docker rm my-devops-container

Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:

Image 18


Docker Compose Commands

Run with Docker Compose:


# Start all services defined in docker-compose.yml
docker-compose up -d

Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:

Image 19

After running the above syntax, it also runs the application and displays it in the browser via http://localhost:3000

# Stop all services and clean up
docker-compose down

Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:
Image 20


Step 9: Deploy to GitHub

We'll now commit the code and push it to GitHub, allowing the automated CI pipeline to begin processing.


 # 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"

Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:

Image 22

IMPORTANT: Before running these commands:

  • Go to GitHub.com and create a new repository, give it a name of your choice. I called mine "my-devops-project"
  • DO NOT initialize it with README, .gitignore, or license (we already have these)
  • Copy the repository URL from GitHub
  • Replace YOUR_GITHUB_USERNAME below with your actual GitHub username

# Set main as the default branch
git branch -M main

# Connect to your GitHub repository (UPDATE THIS URL!)
git remote add origin https://github.com/YOUR_GITHUB_USERNAME/my-devops-project.git

Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:

Image 23


# Push your code to GitHub for the first time
git push -u origin main

Enter fullscreen mode Exit fullscreen mode

Output of command in VScode terminal:

Image 24

What you'll see: Your code appears on GitHub, and the CI/CD pipeline starts running automatically.

Image 25


Step 10: Deploy to Azure Web Apps via Azure Portal

We’ve built our Node.js app, containerized it with Docker, and set up GitHub Actions for CI/CD. Now it’s time to take it live by deploying to Azure Web Apps, a fully managed Platform-as-a-Service (PaaS) that runs your app in the cloud without you managing servers or clusters.

Why Azure Web Apps?

  • Zero server management → Microsoft handles scaling, updates, and runtime.

  • CI/CD ready → GitHub Actions can publish new versions automatically.

  • Scalable → Apps scale up or out based on load.

  • Secure & monitored → Logs, metrics, and diagnostics included by default.

Steps in the Azure Portal

  1. Log in to Azure Portal
  2. Go to Azure Portal and sign in with your Azure account.

    1. Create a Resource Group
  • In the left-hand menu, search for Resource Groups.

  • Click Create → provide a name (e.g., my-devops-app-rg) and select a region (preferably one close to you).

  1. Create an App Service
  • From the menu, search for App Services → click Create and select web app.

Fill in:

  • Resource Group: my-devops-app-rg

  • App Name: my-devops-lab-app (must be unique)

  • Publish: Code

  • Operating System: Linux

  • Region: Choose the same as your resource group

  • Pricing Plan: Start with B1 (Basic) for testing

Image 34

Configure Deployment

In the Deployment tab, enable:

  • Option: Continuous deployment

  • Select Authorize and connect your GitHub account with the Azure App Service to enable access to your repository.

Image 38

Select the following details:

  • Organization: your GitHub account username

  • Repository: your repository name

  • Branch: your branch main (e.g main or master)

Image 39

Review + Create

Click Review + Create and wait for Azure to provision the service.


CI/CD Integration from Portal

Azure Portal can automatically create a GitHub Actions workflow for you:

  • Azure will generate a workflow file in .github/workflows/

  • This workflow builds your app and redeploys automatically whenever you push to GitHub

Select Actions to see the pipeline running:

Image 40

Below depicts the Continuous Integration (CI) pipeline phase:

Image 41b

This depicts the Continuous Deployment (CD) pipeline phase:

Image 41a

Access Your App

This indicates our web app service has been successfully deployed. Select Go to Resource

Image 42

Select Browse to access the application in your browser
Image 44

This is the application live in the browser:

Image 43

Wrapping Up

You just built a complete CI/CD pipeline for a Node.js application:

  • Automated builds & tests with GitHub Actions
  • Containerization with Docker
  • Local orchestration with Docker Compose
  • Cloud hosting on Azure Web Apps

This workflow eliminates manual deployments, ensures code quality, and makes your app production-ready. 🚀

Would you like me to also add screenshots and diagrams (pipeline flow, Kubernetes architecture) to make the Dev.to article more visually engaging?

Top comments (0)