If youβve ever built an app and thought:
- How do I make sure it runs the same everywhere?
- How can I test changes automatically?
- How do I go from my laptop to production safely?
β¦then this guide is for you.
In this hands-on tutorial, weβll set up a complete DevOps pipeline, starting with a simple Node.js app, then moving to automated CI/CD deployments with Docker and Kubernetes.
By the end, you will know how to:
- β‘ Set up Git, Docker, and GitHub Actions
- π§ͺ Write and run automated tests
- π¦ Build lightweight, secure Docker images
- π€ Use CI/CD pipelines for testing, building, and deploying automatically
- π Deploy to staging & production with confidence
π‘ No prior DevOps experience required β just curiosity, code, and a willingness to learn!
-
Node.js (v18 or higher, LTS v20 recommended in 2025)
- Download here
- Verify installation:
node --version npm --version
-
Git (latest stable version)
- Download here
- Verify installation:
git --version
-
Docker Desktop (latest version)
- Download here
- Verify installation:
docker --version docker-compose --version
-
GitHub Account β Sign up here
-
Code Editor (recommended) β Visual Studio Code
β Final Check β Run these:
node --version # v18.x+ or v20.x+
npm --version # v9.x+ or v10.x+
git --version # v2.34+
docker --version # v24.x+
Project Structure
devops-project/
βββ app.js
βββ package.json
βββ Dockerfile
βββ docker-compose.yml
βββ jest.config.js
βββ .eslintrc.js
βββ .dockerignore
βββ .gitignore
βββ .env.example
βββ tests/
β βββ app.test.js
βββ k8s/
β βββ staging/
β β βββ deployment.yml
β βββ production/
β βββ deployment.yml
βββ .github/workflows/
βββ ci.yml
Configure Git:
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
Initialize project:
mkdir devops-project
cd devops-project
git init
npm init -y
Add scripts, metadata, and dev tools:
{
"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"
}
}
const http = require('http');
const url = require('url');
const port = process.env.PORT || 3000;
let requestCount = 0;
const server = http.createServer((req, res) => {
requestCount++;
const { pathname } = url.parse(req.url, true);
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('X-Content-Type-Options', 'nosniff');
switch (pathname) {
case '/':
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`<h1>I'm Getting Better at DevOps, Yay!</h1>`);
break;
case '/health':
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'healthy', uptime: process.uptime() }));
break;
case '/info':
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ platform: process.platform, pid: process.pid }));
break;
case '/metrics':
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`http_requests_total ${requestCount}`);
break;
default:
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not Found' }));
}
});
server.listen(port, () => {
console.log(`π Server running at http://localhost:${port}/`);
});
module.exports = server;
npm install --save-dev jest eslint supertest
npm install
Create a tests/app.test.js file:
const request = require('supertest');
const server = require('../app');
describe('App Endpoints', () => {
afterAll(() => server.close());
test('GET / should return homepage', async () => {
const res = await request(server).get('/');
expect(res.status).toBe(200);
expect(res.text).toContain('DevOps');
});
test('GET /health should return healthy', async () => {
const res = await request(server).get('/health');
expect(res.status).toBe(200);
expect(res.body.status).toBe('healthy');
});
test('GET /info should return system info', async () => {
const res = await request(server).get('/info');
expect(res.status).toBe(200);
expect(res.body.platform).toBeDefined();
});
test('GET /metrics should return metrics', async () => {
const res = await request(server).get('/metrics');
expect(res.status).toBe(200);
expect(res.text).toContain('http_requests_total');
});
test('GET /nonexistent should return 404', async () => {
const res = await request(server).get('/nonexistent');
expect(res.status).toBe(404);
expect(res.body.error).toBe('Not Found');
});
});
module.exports = {
testEnvironment: 'node',
collectCoverage: true,
coverageDirectory: 'coverage',
testMatch: ['**/tests/**/*.test.js']
};
npm test
Create .github/workflows/ci.yml:
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
π³ Step 5: Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY app.js ./
USER node
EXPOSE 3000
CMD ["node", "app.js"]
.dockerignore:
node_modules
.git
coverage
.env
.gitignore:
node_modules/
.env
coverage/
.env.example:
Copy code
PORT=3000
NODE_ENV=production
.eslintrc.js:
module.exports = { extends: ['eslint:recommended'] };
Step 7: Docker Compose
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- PORT=3000
docker-compose up -d
Run locally:
npm install
npm test
npm start
Run with Docker:
docker build -t my-devops-app .
docker run -p 3000:3000 my-devops-app
Run with Compose:
docker-compose up -d
Push to develop β Deploys to staging
Push to main β Deploys to production
GitHub Actions automates tests, builds, scans, and deployments
Kubernetes configs for staging & production included in k8s/.
Conclusion At this point, you now have:
A working Node.js app
Automated tests with Jest
A CI/CD pipeline with GitHub Actions
Docker + Compose for containerization
Kubernetes configs for scalable deployments
Congratulations! Youβve built a full DevOps pipeline from code β CI β Docker β Kubernetes β CD