DEV Community

Cover image for DevOps by Doing: Setting Up a Complete Modern DevOps Environment — Part 1
John Ogbonna
John Ogbonna

Posted on

DevOps by Doing: Setting Up a Complete Modern DevOps Environment — Part 1

Welcome to DevOps by Doing, a hands-on series where we don’t just talk about DevOps principles — we actually build, maintain and upgrade complete modern DevOps environments step by step. This is the first instalment in the series: Setting Up a Complete Modern DevOps Environment. By the end of this exercise series, you’ll have a working Node.js application running through a full DevOps pipeline: from source control to automated testing, containerization, deployment and more.

Instead of abstract theory, we’ll focus on practical implementation. We’ll start small — setting up our project and tools — and then gradually build out the pipeline into a production-ready workflow. Each article will layer on new pieces of the environment so you can follow along at your own pace.

What We’ll Cover in This Series

  • Over the course of this series (likely two to three parts), we’ll walk through the following:

  • Version Control Setup: Initializing Git, configuring .gitignore, and structuring the project repository.

  • Node.js Application Setup: Bootstrapping a simple sample app, installing dependencies, and writing tests.

  • Code Quality & Standards: Setting up ESLint and other linters for clean, consistent code.

  • Environment Management: Creating .env.example files to handle configuration safely.

  • Continuous Integration (CI): Configuring GitHub Actions to run builds, linting, and tests automatically.

  • Containerization: Writing a Dockerfile, setting up .dockerignore, and building images.

  • Local Development with Docker Compose: Creating a docker-compose.yml to run our app and services locally.

  • Continuous Deployment (CD): Extending our GitHub Actions pipeline to build, push, and deploy containers.

Part 1: Laying the Foundation

In this first article, we’ll focus on the core project setup:

  • Initializing Git

  • Setting up a sample Node.js app

  • Installing dependencies

  • Writing and running tests

  • Creating .gitignore and .env.example

  • Adding ESLint for code quality

  • Set up files for GitHub Actions CI/CD

  • Set up Dockerfile

  • Essential Configuration Files such as .gitignore and .dockerignore

By the end of this article, you’ll have a clean, version-controlled project with quality checks in place — ready to move on to Docker, GitHub Actions, and deployment in the upcoming parts.

Let’s get started. 🚀

Requirements
Before we dive in, let’s make sure your development environment is ready. To follow along with this series, you’ll need the following tools installed on your machine:

  • Git – for version control and repository management.

  • Node.js(LTS recommended) – we’ll use it to build our sample application. Installing Node will also give you npm (Node Package Manager).

  • Docker to containerize our application and manage images.

  • Docker Compose to run multi-container environments locally.

  • A GitHub account – since we’ll be pushing code to GitHub and setting up GitHub Actions for CI/CD.

  • A code editor (VS Code recommended) – with useful extensions such as:

    • ESLint
    • Docker

Verify Everything is Installed

Once you’ve installed the required tools, run the following commands in your terminal to make sure everything is set up correctly:

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

check

Step 1: 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.

Git is the backbone of most modern DevOps workflows. It keeps track of changes to your codebase, allows collaboration, and integrates directly with CI/CD pipelines like GitHub Actions.

1.1 Configure Git (if you haven’t already)

Run the following commands to tell Git who you are:

git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
Enter fullscreen mode Exit fullscreen mode

You can confirm your settings with:

git config --list
Enter fullscreen mode Exit fullscreen mode

💡 Use the same email that’s linked to your GitHub account — it makes authentication and contribution tracking easier.

1.2 Initialize a Git Repository

Create and navigate to the folder where you’ll be creating the project and run:

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

This creates a new folder called my-devops-project, moves you inside it, and initializes it as a Git repository. The git init command sets up all the hidden files Git needs (.git/), allowing you to start tracking changes to your project.

git initialized

To confirm that Git has created the repository metadata, list all files (including hidden ones):

ls -a
Enter fullscreen mode Exit fullscreen mode

Expected output (your system may show additional files):

.   ..   .git
Enter fullscreen mode Exit fullscreen mode

show files

1.3 Verify Repository Setup

Check that everything is working by running:

git status
Enter fullscreen mode Exit fullscreen mode

You should see a message saying “On branch main” (or master, depending on your Git version) and that there are no commits yet.
 no commits yet

At this point, Git is ready — next we’ll set up our Node.js application inside this repository. 🚀

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.

2.1 Initialize a Node.js Project

What this step does: Creates a package.json file that describes your project and manages dependencies.

Run the following command inside your project folder:

npm init -y
Enter fullscreen mode Exit fullscreen mode

This generates a package.json file with default values.
package.json

2.2 Update package.json

What this step does: Customizes the package.json with proper scripts and metadata for your DevOps project.

Open the file in your editor (VS Code, for example), or recreate it with:

touch package.json
Enter fullscreen mode Exit fullscreen mode

Then copy and replace the contents with the following:

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

Step 3: Create the Node.js Web Server

What this step does: Creates an HTTP server that listens on port 3000, serves different endpoints (/, /health, /info, /metrics), includes security headers and error handling, provides graceful shutdown capability, and exports the server for testing.

3.1 Create a file calledapp.js. You can do this in VS code or by typing in the terminal:

touch app.js
Enter fullscreen mode Exit fullscreen mode

Copy the following code into a new file called app.js:

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>I'm Getting Better at DevOps, Yay!</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':
      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

Node App

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

3.2 Install Dependencies

What this step does: Downloads and installs the necessary packages for testing and code quality.

Run the following commands:

# 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

What you’ll see:

  • A new node_modules/ folder containing installed packages

  • A package-lock.json file that locks dependency versions for consistency across environments

package-lock.json

Step 4: Create Proper Tests

🎯 What this step does

This step sets up automated testing for your application using Jest and Supertest. Automated tests help verify that your web app’s endpoints behave correctly and prevent regressions when making changes.

4.1 Create Tests Directory and File

First, create a folder to hold your tests and an initial test 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
  • Your project folder should look like this:
my-devops-project/
├── app.js
├── package.json
├── tests/
│   └── app.test.js
Enter fullscreen mode Exit fullscreen mode

folder structure

4.2 Write Your First Tests

Open tests/app.test.js and add the following code:

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

✅ These tests check:

  • / → Welcome page

  • /health → Health check info

  • /info → System details

  • /metrics → Prometheus metrics

  • /nonexistent → Correct 404 handling

4.3 Configure Jest

Next, set up a Jest configuration file so tests run consistently:

touch jest.config.js
Enter fullscreen mode Exit fullscreen mode

Paste the following into jest.config.js:

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

Config breakdown:

  • testEnvironment: 'node' → Run in Node.js context

  • collectCoverage: true → Generate coverage reports

  • coverageDirectory: 'coverage' → Store reports in /coverage

  • testMatch → Look for files ending in .test.js inside /tests

  • verbose: true → Show detailed output

4.4 Run the Tests

Now run the tests with:

npm test
Enter fullscreen mode Exit fullscreen mode

If everything is working, you’ll see output like:
output

Step 5: Start the App 🚀

🎯 What this step does

This step launches your Node.js web server so you can test it locally in your browser and confirm everything works before moving into Docker, CI/CD, and deployment in Part 2.

5.1 Start the Server

Run the following command in your project root:

npm start
Enter fullscreen mode Exit fullscreen mode

You should see output like:

start server

5.2 Verify Endpoints in Browser or curl

Now, open your browser (or use curl) to test the endpoints:

# Welcome page
curl http://localhost:3000/

# Health check
curl http://localhost:3000/health

# Info
curl http://localhost:3000/info

# Metrics
curl http://localhost:3000/metrics
Enter fullscreen mode Exit fullscreen mode
  • Results:
     Results

  • You will see this page on the browser:
    page on the browser

✅ You should see the welcome HTML page at / and JSON responses at /health and /info. The /metrics endpoint will return Prometheus-style metrics.

5.3 Stop the Server

When you’re done testing, stop the server by pressing:

CTRL + C

🎉 Wrapping Up Part 1

In this first part of DevOps by Doing - Setting Up a Complete Modern DevOps Environment, we:

  • Set up Git for version control

  • Created a Node.js project with a sample web app

  • Installed dependencies and dev tools

  • Wrote and ran automated tests with Jest + Supertest

  • Started the app locally

You now have the foundation of a modern DevOps-ready application.

Coming Up in Part 2

In Part 2, we’ll take this project to the next level by:

  • Creating a .gitignore file to keep your repo clean

  • Adding a .env.example for environment variables

  • Setting up ESLint for code quality

  • Writing a Dockerfile and .dockerignore

  • Running your app with Docker Compose

  • Preparing for CI/CD with GitHub Actions

  • Stay tuned — things are about to get even more exciting! 🚀

Top comments (0)