DEV Community

Cover image for Complete CI/CD Pipeline with Node.js, Docker, and Kubernetes
SUBAIR NURUDEEN ADEWALE
SUBAIR NURUDEEN ADEWALE

Posted on

Complete CI/CD Pipeline with Node.js, Docker, and Kubernetes

Introduction

This guide provides a hands-on, end-to-end walkthrough for building, testing, containerizing, and deploying a modern Node.js web application using real-world DevOps best practices.

Instead of focusing only on writing application code, this project is designed to help you understand how software is built, tested, packaged, automated, and deployed in professional environments. By the end of this guide, you will have a fully functional Node.js application with:

  • Automated testing
  • A CI/CD pipeline using GitHub Actions
  • Docker containerization
  • Kubernetes deployment configurations
  • A clear staging and production workflow

The project intentionally uses core Node.js (without frameworks like Express) to help you understand what happens under the hood, making it ideal for DevOps engineers, backend developers, cloud engineers, and beginners transitioning into DevOps.

This guide follows a step-by-step approach, ensuring you can complete the project in short sessions while gaining practical, job-ready skills.

Objectives

By completing this project, you will be able to:

Build a production-ready Node.js web application

  • Create a basic HTTP server
  • Serve HTML pages and JSON APIs
  • Implement health checks, metrics, and system info endpoints

Apply software engineering best practices

  • Use Git for version control
  • Write automated tests with Jest and Supertest
  • Enforce code quality using ESLint

Implement CI/CD with GitHub Actions

  • Automatically run tests and linting on every push
  • Build and push Docker images to a container registry
  • Scan container images for security vulnerabilities

Containerize applications using Docker

  • Create optimized, multi-stage Docker images
  • Run applications securely as a non-root user
  • Configure health checks and proper signal handling

Run and manage applications locally with Docker Compose

  • Simplify local development and testing
  • Manage environment variables and service health

Prepare applications for Kubernetes deployment

  • Create staging and production Kubernetes manifests
  • Configure replicas, probes, and resource limits
  • Follow environment-based deployment strategies

Prepare applications for Kubernetes deployment

  • Create staging and production Kubernetes manifests
  • Configure replicas, probes, and resource limits
  • Follow environment-based deployment strategies

Understand real-world DevOps workflows

  • Use branch-based deployments (develop - staging, main - production)
  • Monitor CI/CD pipelines and container builds
  • Troubleshoot common DevOps and deployment issues

Prerequisites – Download These First

Before we start building the CI/CD pipeline, you need to install a few essential tools. These tools form the foundation of the development, containerization, and automation workflow used throughout this project.

Node.js (v18 or higher)

  • Node.js is the runtime used to build and run our web application.
  • Current LTS (2025): v20.x
  • Download: https://nodejs.org/
  • Choose the LTS version for stability

Verify installation:
node --version
npm --version

Expected output:

Node.js: v18.x+ or v20.x+

npm: 9.x+ or 10.x+

Git (Latest Stable Version)

Git is used for version control and collaboration. It also triggers our CI/CD pipeline when code is pushed to GitHub.

Verify installation:
git --version

Expected output:

Git version 2.34+

Docker Desktop (Latest Version)

Docker allows us to package the application into containers so it runs the same everywhere — solving the classic “it works on my machine” problem.

Verify installation:

docker --version
docker-compose --version

Expected output:

Docker version 24.x+

Docker Compose available and running

GitHub Account

  • GitHub hosts your source code and runs the automated CI/CD pipeline using GitHub Actions.
  • Sign up: https://github.com
  • You will use GitHub to:
  • Store your project code
  • Trigger automated tests
  • Build Docker images
  • Deploy to staging and production environments

Code Editor (Optional but Recommended)

A code editor makes writing, reading, and managing your code much easier.

Recommended:

Final Verification Checklist

Run the following commands to confirm everything is installed correctly:

node --version # v18.x+ or v20.x+
npm --version # 9.x+ or 10.x+
git --version # 2.34+
docker --version # 24.x+

If all commands return valid versions, you’re ready to move on.

Step 1: Set Up Git for Version Control

What this step does: it 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"

1.1 Select** Terminal** at the top of visual studio code and select *new terminal *

1.2 Enter the One-time Git Configuration

What this step does: it Configures Git on your machine so it knows who you are when you make commits, and sets up proper project tracking.

1.3 Create and Initialize Project

mkdir my-devops-project
cd my-devops-project
git init

mkdir my-devops-project
Creates a new folder (directory) named my-devops-project.

cd my-devops-project
What it does: Moves you into the newly created folder, Your terminal is now pointing to: .../my-devops-project Anything you create next will live inside this folder.

git init
What it does: Initializes a new Git repository in the current folder
Behind the scenes: Git creates a hidden .git/ directory This folder stores: Version history, Branches, Commits and configurations.

code .
use code . to open the current folder in Visual Studio Code.

Current folder (my-devops-project)

Step 2: Build a Node.js Web App

What this step does: Creates a web application using Node.js that can serve web pages and API endpoints.

Initialize Node.js Project using npm init -y
What this step does: Creates a package.json file that describes your project and manages dependencies.

2.1 Update package.json

Open the package.json file and update it with the appropriate project details and scripts that will be used throughout the CI/CD pipeline.
This includes:

  • Project name and description
  • Entry point (app.js or server.js)
  • Start and test scripts
  • Metadata used during builds and deployments
{
  "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

*2.2 Create Application File (app.js) *
What this code does:

  • Creates an HTTP server that listens on port 3000
  • Serves different endpoints (/, /health, /info, /metrics)
  • Includes security headers and proper error handling
  • Provides graceful shutdown capability
  • Exports the server for testing

Create app.js file using touch app.js or New-item app.js.

the app.js file is the entry point i.e the starting point of our application it contains configurations. It is the file that Node.js loads first when the application starts.

Copy this code into the applicagion file app.js.

// 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>DevOps Lab 2025</h1>
    <p>Modern Node.js application with CI/CD pipeline</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

2.3 Install Dependencies

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

Install all dependencies (creates node_modules folder)
npm install

What you'll see:

A node_modules/ folder with all installed packages
A package-lock.json file that locks dependency versions

Step 3: Create Proper Tests

What this step does: Sets up automated testing so you can verify your application works correctly every time you make changes.

3.1 Create tests directory and test file

Create a folder for your tests
mkdir tests

Create the main test file
touch tests/app.test.js or New-item tests/app.test.js

Copy this code 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

Create jest.config.js using the command touch jest.config.js or New-item jest.config.js

Copy the code into 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

What this step does: Creates an automated pipeline that runs tests and builds Docker images every time you push code to GitHub.

4.1 Create workflow directory
Create the GitHub Actions directory structure using the command

mkdir -p .github/workflows

4.2 Create CI/CD pipeline file
Create .github/workflows/ci.yml using the command touch .github/workflows/ci.yml or New-item github/workflows/ci.yml

4.3 Copy this code into the CI/CD Pipeline file.

name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
    tags: [ 'v*' ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    name: Test & Lint
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [20, 22]

    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: npm run lint

      - name: Run tests
        run: npm test

      - name: Security audit
        run: npm audit --audit-level=critical || echo "Audit completed with warnings"

  build:
    name: Build & Push Image
    runs-on: ubuntu-latest
    needs: test
    if: github.event_name == 'push'

    permissions:
      contents: read
      packages: write
      security-events: write

    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          platforms: linux/amd64,linux/arm64

      - 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=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix={{branch}}-
            type=raw,value=${{ github.run_id }}
            type=raw,value=latest,enable={{is_default_branch}}
          labels: |
            org.opencontainers.image.title=DevOps Lab 2025
            org.opencontainers.image.description=Modern Node.js DevOps application

      - 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
          target: production

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@0.24.0
        with:
          image-ref: ${{ steps.meta.outputs.tags }}
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
        continue-on-error: true

      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v3
        if: always() && hashFiles('trivy-results.sarif') != ''
        with:
          sarif_file: 'trivy-results.sarif'

  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 "Digest: ${{ needs.build.outputs.image-digest }}"
          # Add your staging deployment commands here (kubectl, helm, etc.)

  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 "Digest: ${{ needs.build.outputs.image-digest }}"
          # Add your production deployment commands here
Enter fullscreen mode Exit fullscreen mode

Step 5: Dockerfile

What this step does: Creates instructions for Docker to build a container image of your application that can run anywhere.

What this Dockerfile does:

  • Uses multi-stage builds for smaller image size
  • Installs curl for health checks
  • Creates a non-root user for security
  • Sets up proper file permissions
  • Configures health checks

Create Dockerfile:

create a dockerfile using the command touch Dockerfile or New-item Dockerfile

Copy the docker instructions into the 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

What this step does: Creates configuration files that tell various tools what to ignore, how to behave, and what settings to use.

Create .dockerignore using the command touch .dockerignore or New-item .dockerignore

Copy this code into the .dockerignore file.

# Node.js dependencies
node_modules
npm-debug.log*
yarn-error.log*

# Git & CI/CD
.git
.github

# Environment variables
.env
.env.local
.env.*.local

# Logs
logs
*.log

# Test & coverage files
tests/
coverage
.nyc_output
jest.config.js

# Linting & config files
.eslintrc*
README.md

# IDE & editor files
.vscode
.idea
*.swp
*.swo

# OS-specific files
.DS_Store
Thumbs.db
Enter fullscreen mode Exit fullscreen mode

6.1 Create .gitignore
Create .gitignore usinf the command touch .gitignore or New-item .gitignore

Copy this code into the .gitignore file.

# Node.js dependencies
node_modules
npm-debug.log*
yarn-error.log*

# Git & CI/CD
.git
.github

# Environment variables
.env
.env.local
.env.*.local

# Logs
logs
*.log

# Test & coverage files
tests/
coverage
.nyc_output
jest.config.js

# Linting & config files
.eslintrc*
README.md

# IDE & editor files
.vscode
.idea
*.swp
*.swo

# OS-specific files
.DS_Store
Thumbs.db
Enter fullscreen mode Exit fullscreen mode

6.2 Create environment template
Create .env.example using the command touch .env.example or touch New-item .env.example

Copy this configuration into the .env.example file and save.

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

6.3 Create ESLint configuration
Create .eslintrc.js file using the command touch .eslintrc.js or New-item .eslintrc.js

Copy this code into the .eslintrc.js file and save.

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

What this step does: Creates a Docker Compose file that makes it easy to run your application and any supporting services with a single command.

Create docker-compose.yml using the command touch docker-compose.yml or New-item docker-compose.yml

Copy this code into the docker-compose.yml file and save.

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

What this step does: Shows you how to actually run and test your application locally before deploying it.

Install and Test Locally
Install all dependencies from package.json using the command npm install

Run your test suite to make sure everything works using the command
npm test

Start the application server using the command
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) by copying and pasting each into a terminal.
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

Copy the url localhost:3000 into a browser to see your application.

Docker Commands
Start docker desktop application to start the docker engine and run the docker command to build the image
# Build image

docker build -t my-devops-app:latest .

Navigate to docker desktop to see the image

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

Navigate to docker desktop to see the container

Check container status
docker ps

docker logs my-devops-container

Test health check
curl http://localhost:3000/health

Stop container
docker stop my-devops-container

Navigate to Docker desktop to see the stopped container

Remove the container
docker rm my-devops-container

Navigate to Docker desktop to see if the container as been removed.

Docker Compose Commands

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

Step 9: Deploy to GitHub

What this step does: Commits your code to Git and pushes it to GitHub so the automated CI/CD pipeline can start working.
Initial commit

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
IMPORTANT: Before running these commands:

  • Go to GitHub.com and create a new repository called 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
  • # Push your code to GitHub for the first time git push origin main

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

Step 10: Kubernetes Deployment Configurations

What this step does: Creates Kubernetes configuration files that define how your application should run in staging and production environments.

Create directories
Create directories for Kubernetes configurations
mkdir -p k8s/staging

*Create Staging Deployment file *

Create k8s/staging/deployment.yml file using touch k8s/staging/deployment.yml or New-item k8s/staging/deployment.yml

Copy this code into the deployment file
NOTE: Update YOUR_GITHUB_USERNAME in the image URL below!

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/YOUR_GITHUB_USERNAME/my-devops-project:develop-latest
        ports:
        - containerPort: 3000
        env:
        - name: NODE_ENV
          value: "staging"
        - name: PORT
          value: "3000"
        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
Enter fullscreen mode Exit fullscreen mode

Create Production Deployment

Create k8s/production directory using the command mkdir k8s/production and create the depployment file touch k8s/production/deployment.yml or New-item k8s/production/deployment.yml

Copy this code into the k8s/production/deployment.yml file

NOTE: Update YOUR_GITHUB_USERNAME in the image URL below

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/YOUR_GITHUB_USERNAME/my-devops-project: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

Step 11: 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
use the command 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.

Monitor Deployments

Check GitHub Actions status

Visit: https://github.com/YOUR_GITHUB_USERNAME/my-devops-project/actions

Check your container registry
Visit: https://github.com/YOUR_GITHUB_USERNAME/REPO NAME/pkgs/container/REPO NAME

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

Conclusion
This project successfully demonstrates a complete DevOps workflow for modern web application development. We transformed a basic Node.js application into a professional, automated system with CI/CD pipelines, containerization, and deployment strategies.

Key Achievements
End-to-End Automation: GitHub Actions automates testing, building, and deployment from code to production
Containerization: Docker creates secure, portable images that run consistently anywhere
Professional Testing: Jest and Supertest ensure application reliability with automated tests
Security Integration: Trivy vulnerability scanning and security headers protect the application
Kubernetes Ready: Production-grade deployment configurations for staging and production
Branch-Based Workflow: Safe deployments using develop/staging and main/production branches

Top comments (0)