diff --git a/2026/day-36/backend/Dockerfile b/2026/day-36/backend/Dockerfile new file mode 100755 index 0000000000..4b8dfdad91 --- /dev/null +++ b/2026/day-36/backend/Dockerfile @@ -0,0 +1,14 @@ +# Taking the base image +FROM node:18-alpine +# Set the working directory +WORKDIR /app +#Copy the package +COPY package.json . +# Run the code to install all +RUN npm install +# Copy the code in my container directory from the local +COPY . . +# Expose the port +EXPOSE 5000 +# Serve the code +CMD ["node","index.js"] diff --git a/2026/day-36/backend/index.js b/2026/day-36/backend/index.js new file mode 100755 index 0000000000..3980f2211e --- /dev/null +++ b/2026/day-36/backend/index.js @@ -0,0 +1,45 @@ +const express = require('express'); +const mysql = require('mysql2'); +const cors = require('cors'); +const app = express(); +app.use(cors()); +app.use(express.json()); +const db = mysql.createPool({ + host: process.env.DB_HOST || 'mysql', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || 'rootpassword', + database: process.env.DB_NAME || 'tododb', + waitForConnections: true, + connectionLimit: 10, +}); +app.get('/health', (req, res) => res.json({ status: 'ok' })); +app.get('/todos', (req, res) => { + db.query('SELECT * FROM todos ORDER BY created_at DESC', (err, results) => { + if (err) return res.status(500).json({ error: err.message }); + res.json(results); + }); +}); +app.post('/todos', (req, res) => { + const { title } = req.body; + if (!title) return res.status(400).json({ error: 'Title is required' }); + db.query('INSERT INTO todos (title) VALUES (?)', [title], (err, result) => { + if (err) return res.status(500).json({ error: err.message }); + res.status(201).json({ id: result.insertId, title, completed: false }); + }); +}); +app.put('/todos/:id', (req, res) => { + const { id } = req.params; + db.query('UPDATE todos SET completed = NOT completed WHERE id = ?', [id], (err) => { + if (err) return res.status(500).json({ error: err.message }); + res.json({ message: 'Updated' }); + }); +}); +app.delete('/todos/:id', (req, res) => { + const { id } = req.params; + db.query('DELETE FROM todos WHERE id = ?', [id], (err) => { + if (err) return res.status(500).json({ error: err.message }); + res.json({ message: 'Deleted' }); + }); +}); +const PORT = process.env.PORT || 5000; +app.listen(PORT, () => console.log('Backend running on port ' + PORT)); diff --git a/2026/day-36/backend/package.json b/2026/day-36/backend/package.json new file mode 100755 index 0000000000..14c368adc3 --- /dev/null +++ b/2026/day-36/backend/package.json @@ -0,0 +1,11 @@ +{ + "name": "todo-backend", + "version": "1.0.0", + "main": "index.js", + "scripts": { "start": "node index.js" }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "mysql2": "^3.6.0" + } +} diff --git a/2026/day-36/day-36-docker-project.md b/2026/day-36/day-36-docker-project.md new file mode 100644 index 0000000000..9ea4924058 --- /dev/null +++ b/2026/day-36/day-36-docker-project.md @@ -0,0 +1,57 @@ +# Day 36 - Docker Project: Dockerized Todo App + +## Project Overview +A full-stack Todo application fully containerized using Docker and Docker Compose. +The app consists of three services: React frontend, Node.js backend, and MySQL database. + +## Architecture +- **Frontend**: React app served via Nginx on port 3000 +- **Backend**: Node.js + Express REST API on port 5000 +- **Database**: MySQL 8 on port 3306 +- All services connected via custom Docker bridge network + +## Docker Hub Images +- Frontend: mohammadadnankhan/todo-frontend +- Backend: mohammadadnankhan/todo-backend +- MySQL: mohammadadnankhan/mysql-cont + +## How to Run +```bash +git clone git@github.com:AddyKhan257/To-Do-App.git +cd To-Do-App +docker-compose up -d +``` +Open browser: http://localhost:3000 + +## Dockerfile Decisions +### Backend +- Used node:18-alpine for small image size +- Copied package.json first to leverage Docker layer caching +- Used CMD instead of ENTRYPOINT for flexibility + +### Frontend (Multi-stage) +- Stage 1: node:18-alpine to build React app +- Stage 2: nginx:alpine to serve static files +- Final image reduced from 800MB to 25MB + +## docker-compose Decisions +- Custom bridge network for container communication +- Healthcheck on MySQL so backend waits for DB +- depends_on with condition: service_healthy for startup order +- init.sql mounted to docker-entrypoint-initdb.d for automatic DB setup +- Named volume for MySQL data persistence + +## Challenges +- Fixed DB_HOST to use service name instead of localhost +- Debugged database name mismatch between init.sql and compose file +- Fixed WSL permission issues with git + +## Key Commands +```bash +docker build -t todo-backend ./backend +docker build -t todo-frontend ./frontend +docker-compose up -d +docker-compose logs backend +docker-compose down -v +docker ps +``` diff --git a/2026/day-36/docker-compose.yml b/2026/day-36/docker-compose.yml new file mode 100755 index 0000000000..71c6ba6d0a --- /dev/null +++ b/2026/day-36/docker-compose.yml @@ -0,0 +1,60 @@ +version: "3.8" + +services: + + + mysql: + container_name: mysql_todoapp + image: mohammadadnankhan/mysql-cont + environment: + MYSQL_ROOT_PASSWORD: Test@123 + MYSQL_DATABASE: tododb + volumes: + - mysql-vol:/var/lib/mysql + - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - todo-app-nw + ports: + - "3306:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-pTest@123"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + + backend: + container_name: todo-backend + image: mohammadadnankhan/todo-backend + environment: + DB_HOST: mysql + DB_USER: root + DB_PASSWORD: Test@123 + DB_NAME: tododb + networks: + - todo-app-nw + ports: + - "5000:5000" + depends_on: + mysql: + condition: service_healthy + + + frontend: + container_name: todo-frontend + image: mohammadadnankhan/todo-frontend + ports: + - "3000:80" + networks: + - todo-app-nw + depends_on: + - backend + + +networks: + todo-app-nw: + driver: bridge + +volumes: + mysql-vol: \ No newline at end of file diff --git a/2026/day-36/frontend/Dockerfile b/2026/day-36/frontend/Dockerfile new file mode 100755 index 0000000000..48746bcf73 --- /dev/null +++ b/2026/day-36/frontend/Dockerfile @@ -0,0 +1,25 @@ +##STAGE 1## +##--BUILD--## + +# taking the base image +FROM node:18-alpine AS BUILD +# creating the directory for the container +WORKDIR /app +# Copy the package.json in container directory from local to avoid repeating the downloads of dependencies +COPY package.json . +# Run the commands to install all +RUN npm install +# Copy the code from my local +COPY . . +# This create /app/build folder containing pure HTML, CSS, JS files β€” no Node.js needed anymore! +RUN npm run build + +##STAGE 2## +##SERVE## + +# Start fresh with a tiny Nginx image. Everything from Stage 1 is thrown away except what we copy! +FROM nginx:alpine +# Copy ONLY the built files from Stage 1 into Nginx +COPY --from=BUILD /app/build /usr/share/nginx/html +# Nginx serves on port 80. +EXPOSE 80 diff --git a/2026/day-36/frontend/package.json b/2026/day-36/frontend/package.json new file mode 100755 index 0000000000..1aba3ba50f --- /dev/null +++ b/2026/day-36/frontend/package.json @@ -0,0 +1,17 @@ +{ + "name": "todo-frontend", + "version": "1.0.0", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build" + }, + "browserslist": { + "production": [">0.2%", "not dead", "not op_mini all"], + "development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"] + } +} diff --git a/2026/day-36/frontend/public/index.html b/2026/day-36/frontend/public/index.html new file mode 100755 index 0000000000..81fac08a59 --- /dev/null +++ b/2026/day-36/frontend/public/index.html @@ -0,0 +1,11 @@ + + + + + + Todo App + + +
+ + diff --git a/2026/day-36/frontend/src/App.js b/2026/day-36/frontend/src/App.js new file mode 100755 index 0000000000..a8d52a3b8c --- /dev/null +++ b/2026/day-36/frontend/src/App.js @@ -0,0 +1,52 @@ +import { useState, useEffect } from "react"; +const API = "http://localhost:5000"; +export default function App() { + const [todos, setTodos] = useState([]); + const [input, setInput] = useState(""); + const fetchTodos = () => + fetch(`${API}/todos`).then((r) => r.json()).then(setTodos); + useEffect(() => { fetchTodos(); }, []); + const addTodo = () => { + if (!input.trim()) return; + fetch(`${API}/todos`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title: input }), + }).then(() => { setInput(""); fetchTodos(); }); + }; + const toggleTodo = (id) => + fetch(`${API}/todos/${id}`, { method: "PUT" }).then(fetchTodos); + const deleteTodo = (id) => + fetch(`${API}/todos/${id}`, { method: "DELETE" }).then(fetchTodos); + return ( +
+

πŸ“ Todo App

+

Dockerized with React + Node.js + MySQL

+
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && addTodo()} + placeholder="Add a new task..." + style={{ flex: 1, padding: "10px 14px", borderRadius: 8, border: "1px solid #ddd", fontSize: 15 }} + /> + +
+ {todos.length === 0 &&

No todos yet. Add one above!

} + {todos.map((todo) => ( +
+ toggleTodo(todo.id)} + style={{ cursor: "pointer", textDecoration: todo.completed ? "line-through" : "none", color: todo.completed ? "#aaa" : "#333", flex: 1 }}> + {todo.completed ? "βœ…" : "⬜"} {todo.title} + + +
+ ))} +
+ ); +} diff --git a/2026/day-36/frontend/src/index.js b/2026/day-36/frontend/src/index.js new file mode 100755 index 0000000000..c096da5726 --- /dev/null +++ b/2026/day-36/frontend/src/index.js @@ -0,0 +1,5 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/2026/day-36/mysql/init.sql b/2026/day-36/mysql/init.sql new file mode 100755 index 0000000000..893f8c5951 --- /dev/null +++ b/2026/day-36/mysql/init.sql @@ -0,0 +1,14 @@ +CREATE DATABASE IF NOT EXISTS tododb; +USE tododb; + +CREATE TABLE IF NOT EXISTS todos ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + completed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO todos (title) VALUES + ('Learn Docker'), + ('Write Dockerfile'), + ('Set up Docker Compose'); diff --git a/2026/day-37/day-37-revision.md b/2026/day-37/day-37-revision.md new file mode 100644 index 0000000000..6f30e31b97 --- /dev/null +++ b/2026/day-37/day-37-revision.md @@ -0,0 +1,220 @@ +# πŸ“ Day 37 - Docker Revision & Self-Check +> By Mohammad Adnan Khan | 90DaysOfDevOps + +--- + +## βœ… Self-Check Questions & Answers + +### Q1: What is the difference between a Docker Image and a Container? + +**Answer:** +- **Docker Image** is a blueprint/recipe used to create containers. It is read-only and contains everything needed to run an app β€” code, runtime, dependencies, and configs. Like a class in programming. +- **Docker Container** is a running instance of an image. It is lightweight because it shares the host OS kernel instead of using a hypervisor like a VM. Like an object created from a class. + +> πŸ’‘ Key difference: Image = static blueprint. Container = running process. + +--- + +### Q2: What is a Dockerfile? Name 5 instructions with their purpose. + +**Answer:** +A Dockerfile is a text file with instructions to build a Docker image step by step. + +| Instruction | Purpose | +|---|---| +| `FROM` | Sets the base image (e.g., node:18-alpine) | +| `WORKDIR` | Creates and sets working directory inside container | +| `COPY` | Copies files from host machine into container | +| `RUN` | Executes commands during image build (e.g., npm install) | +| `CMD` | Sets default command to run when container starts | +| `EXPOSE` | Documents which port the container listens on | + +--- + +### Q3: What is Docker Layer Caching? Why do we COPY package.json before COPY . .? + +**Answer:** +Docker builds images in layers and caches each layer. If a layer hasn't changed, Docker reuses the cached version instead of rebuilding it. + +We copy `package.json` first because: +1. `package.json` contains the list of all dependencies +2. If we `COPY . .` first, any code change triggers `npm install` again (slow!) +3. By copying `package.json` first β†’ `npm install` layer is cached β†’ only reruns when dependencies actually change +4. Then `COPY . .` copies our code changes β†’ much faster builds! +```dockerfile +COPY package.json . # cached unless dependencies change +RUN npm install # cached! only reruns if package.json changes +COPY . . # only this layer rebuilds when code changes +``` + +--- + +### Q4: What is the difference between CMD and ENTRYPOINT? + +**Answer:** + +| | CMD | ENTRYPOINT | +|---|---|---| +| Can override? | βœ… Yes | ❌ No | +| Used for | Default commands, web servers | Executable containers | +| Override how | `docker run myapp npm test` | Must use `--entrypoint` flag | + +- **CMD** = a suggestion. "Run this by default but you can change it" +- **ENTRYPOINT** = a rule. "Always run this no matter what" + +We used `CMD ["node","index.js"]` in our backend because we want flexibility to override it for debugging. + +--- + +### Q5: What is a Multi-Stage Dockerfile? Why did we use it for frontend? + +**Answer:** +A multi-stage Dockerfile uses multiple `FROM` statements. Each `FROM` starts a new stage. + +**Why for frontend:** +- Stage 1 uses `node:18-alpine` to BUILD the React app β†’ creates `/app/build` folder with HTML/CSS/JS +- Stage 2 uses `nginx:alpine` to SERVE those built files +- Final image only contains Stage 2 (tiny Nginx + built files) +- Result: ~25MB instead of ~800MB! +```dockerfile +# Stage 1 - Build (thrown away after) +FROM node:18-alpine AS BUILD +RUN npm run build + +# Stage 2 - Serve (final image) +FROM nginx:alpine +COPY --from=BUILD /app/build /usr/share/nginx/html +``` + +--- + +### Q6: What is Docker Compose and why do we need it? + +**Answer:** +Docker Compose is a tool to define and run multi-container applications using a single YAML file (`docker-compose.yml`). + +Instead of running 3 separate `docker run` commands for frontend, backend, and MySQL β€” we write one `docker-compose.yml` and run: +```bash +docker-compose up +``` + +It handles: +- Building images +- Creating networks +- Setting up volumes +- Passing environment variables +- Managing startup order with `depends_on` + +--- + +### Q7: Why is DB_HOST: mysql and not DB_HOST: localhost? + +**Answer:** +Each Docker container has its own isolated network namespace with its own `localhost`. If backend uses `DB_HOST: localhost`, it looks for MySQL **inside itself** β€” which doesn't exist! + +In Docker networks, containers are reachable by their **service name**. Docker acts as a DNS server β€” `mysql` resolves to the IP of the MySQL container automatically. +```yaml +DB_HOST: mysql # βœ… finds MySQL container by service name +DB_HOST: localhost # ❌ looks inside backend container itself! +``` + +--- + +### Q8: What is a Docker Volume? Why did only MySQL need one? + +**Answer:** +A Docker volume is persistent storage that exists outside the container. Data stored in a volume survives container deletion and restarts. + +**Why only MySQL:** +- **MySQL** writes all data (tables, rows) to disk β†’ without volume, deleting container = losing all data forever! ❌ +- **Backend** is stateless β€” processes requests and sends data to MySQL, nothing stored locally βœ… +- **Frontend** static files are baked into the image β€” no dynamic data to persist βœ… + +> Rule: Any service that writes data to disk needs a volume! + +--- + +### Q9: What does this line do in docker-compose.yml? +```yaml +- ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql +``` + +**Answer:** +This is a **bind mount** that maps a local file into a special MySQL folder. + +- `./mysql/init.sql` β†’ SQL file on our local machine +- `/docker-entrypoint-initdb.d/` β†’ Special folder inside MySQL container. Any `.sql` file placed here is **automatically executed** by MySQL on first startup. + +So when MySQL container starts for the first time: +1. It finds `init.sql` in that special folder +2. Runs it automatically +3. Creates `tododb` database βœ… +4. Creates `todos` table βœ… +5. Inserts sample data βœ… + +--- + +### Q10: What is a Healthcheck in Docker Compose? Why did we add it to MySQL? + +**Answer:** +A healthcheck periodically checks if a service is actually ready to accept connections (not just started). +```yaml +healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s +``` + +**Why MySQL needs it:** +- `depends_on` alone only waits for the container to START +- MySQL takes time to initialize even after starting +- Without healthcheck β†’ backend tries to connect before MySQL is ready β†’ crash! ❌ +- With healthcheck β†’ backend waits until MySQL passes health check β†’ connects successfully βœ… + +We used `condition: service_healthy` in backend's `depends_on` to ensure proper startup order. + +--- + +## πŸ“Š My Revision Score + +| Question | Topic | Score | +|---|---|---| +| Q1 | Image vs Container | βœ… 10/10 | +| Q2 | Dockerfile Instructions | βœ… 10/10 | +| Q3 | Layer Caching | βœ… 9/10 | +| Q4 | CMD vs ENTRYPOINT | ⚠️ 6/10 (forgot details) | +| Q5 | Multi-Stage Build | βœ… 9/10 | +| Q6 | Docker Compose | βœ… 10/10 | +| Q7 | DB_HOST service name | βœ… 10/10 | +| Q8 | Volumes | βœ… 8/10 | +| Q9 | init.sql mount | βœ… 9/10 | +| Q10 | Healthcheck | βœ… 10/10 | + +**Overall: 9.1/10** πŸ”₯ + +--- + +## 🎯 Key Takeaways from Days 29-36 + +1. Docker solves "works on my machine" problem by packaging app + environment together +2. Always use Alpine base images for smaller image sizes +3. Layer caching = copy dependencies first, then code +4. Multi-stage builds = build in big image, serve in tiny image +5. Never use localhost for inter-container communication β€” use service names! +6. Volumes = only for stateful services (databases) +7. Healthchecks = ensure services are READY not just started +8. docker-compose = one command to rule them all πŸ”₯ + +--- + +## πŸ”— References +- GitHub: [AddyKhan257/To-Do-App](https://github.com/AddyKhan257/To-Do-App) +- Docker Hub: [mohammadadnankhan](https://hub.docker.com/u/mohammadadnankhan) +- LinkedIn: [Mohammad Adnan Khan](https://www.linkedin.com/in/mohammad-adnan-khan-8099802b1) + +--- + +*Day 37 of 90DaysOfDevOps Challenge* +*#90DaysOfDevOps #DevOpsKaJosh #TrainWithShubham* diff --git a/2026/day-37/docker-cheatsheet.md b/2026/day-37/docker-cheatsheet.md new file mode 100644 index 0000000000..dd7367a8f6 --- /dev/null +++ b/2026/day-37/docker-cheatsheet.md @@ -0,0 +1,267 @@ +# 🐳 Docker Cheat Sheet +> By Mohammad Adnan Khan | 90DaysOfDevOps Day 37 + +--- + +## πŸ“¦ Core Concepts + +| Concept | Description | +|---|---| +| **Image** | Blueprint/recipe to create containers. Like a class in programming. | +| **Container** | Running instance of an image. Like an object from a class. | +| **Dockerfile** | Instructions to build a Docker image. | +| **Docker Compose** | Tool to run multiple containers together using a YAML file. | +| **Volume** | Persistent storage that survives container restarts/deletions. | +| **Network** | Allows containers to communicate with each other. | +| **Registry** | Storage for Docker images. (Docker Hub is the default) | + +--- + +## πŸ› οΈ Dockerfile Instructions + +| Instruction | Purpose | Example | +|---|---|---| +| `FROM` | Set base image | `FROM node:18-alpine` | +| `WORKDIR` | Set working directory inside container | `WORKDIR /app` | +| `COPY` | Copy files from host to container | `COPY package.json .` | +| `RUN` | Execute command during build | `RUN npm install` | +| `EXPOSE` | Document which port container listens on | `EXPOSE 5000` | +| `CMD` | Default command when container starts (overridable) | `CMD ["node","index.js"]` | +| `ENTRYPOINT` | Command that always runs (not overridable) | `ENTRYPOINT ["nginx"]` | +| `ENV` | Set environment variables | `ENV NODE_ENV=production` | +| `ARG` | Build-time variables | `ARG VERSION=1.0` | +| `VOLUME` | Create mount point for volumes | `VOLUME /data` | + +--- + +## ⚑ CMD vs ENTRYPOINT + +| | CMD | ENTRYPOINT | +|---|---|---| +| **Can override?** | βœ… Yes easily | ❌ Not easily | +| **Used for** | Default start commands | When container = executable | +| **Override example** | `docker run myapp npm test` | Must use `--entrypoint` flag | +| **Best for** | Web servers, APIs | CLI tools, scripts | + +--- + +## πŸ—οΈ Multi-Stage Build +```dockerfile +# Stage 1 - BUILD (big image, just for building) +FROM node:18-alpine AS BUILD +WORKDIR /app +COPY package.json . +RUN npm install +COPY . . +RUN npm run build + +# Stage 2 - SERVE (tiny image, just for serving) +FROM nginx:alpine +COPY --from=BUILD /app/build /usr/share/nginx/html +EXPOSE 80 +``` + +**Why?** +- Without multi-stage β†’ ~800MB image 😱 +- With multi-stage β†’ ~25MB image βœ… +- Stage 1 builds, Stage 2 serves. Only Stage 2 ends up in final image! + +--- + +## ⚑ Docker Layer Caching +```dockerfile +# βœ… CORRECT - Cache friendly +COPY package.json . # Layer 1 - only changes if dependencies change +RUN npm install # Layer 2 - cached! only reruns if package.json changes +COPY . . # Layer 3 - your code changes here + +# ❌ WRONG - Slow builds +COPY . . # copies everything first +RUN npm install # reruns every time any file changes! +``` + +**Rule:** Copy dependency files first β†’ install β†’ copy code! + +--- + +## 🐳 Essential Docker Commands + +### Images +```bash +docker build -t myimage . # Build image from Dockerfile +docker build -t myimage ./folder # Build from specific folder +docker images # List all images +docker rmi myimage # Remove image +docker rmi $(docker images -q) -f # Remove ALL images +docker pull nginx # Pull image from Docker Hub +docker push username/myimage # Push image to Docker Hub +``` + +### Containers +```bash +docker run myimage # Run container +docker run -d myimage # Run in background (detached) +docker run -p 3000:80 myimage # Map port host:container +docker run -e DB_HOST=mysql myimage # Pass environment variable +docker ps # List running containers +docker ps -a # List all containers +docker stop container_id # Stop container +docker rm container_id # Remove container +docker logs container_id # See container logs +docker exec -it container_id sh # Enter container shell +``` + +### Docker Compose +```bash +docker-compose up # Start all services +docker-compose up -d # Start in background +docker-compose down # Stop and remove containers +docker-compose down -v # Stop, remove containers + volumes +docker-compose logs backend # See logs of specific service +docker-compose ps # List compose services +docker-compose build # Build/rebuild services +docker-compose restart # Restart all services +``` + +### Volumes & Networks +```bash +docker volume ls # List volumes +docker volume rm volume_name # Remove volume +docker network ls # List networks +docker network inspect network_name # Inspect network +``` + +--- + +## πŸ“ docker-compose.yml Structure +```yaml +version: "3.8" + +services: + mysql: + image: mysql:8 + container_name: mysql_app + environment: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: mydb + volumes: + - mysql-vol:/var/lib/mysql + - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - app-network + ports: + - "3306:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + backend: + build: ./backend + container_name: app-backend + environment: + DB_HOST: mysql + DB_USER: root + DB_PASSWORD: password + DB_NAME: mydb + networks: + - app-network + ports: + - "5000:5000" + depends_on: + mysql: + condition: service_healthy + + frontend: + build: ./frontend + container_name: app-frontend + ports: + - "3000:80" + networks: + - app-network + depends_on: + - backend + +networks: + app-network: + driver: bridge + +volumes: + mysql-vol: +``` + +--- + +## 🌐 Docker Networking + +| Network Type | Description | Use Case | +|---|---|---| +| **bridge** | Default. Containers on same host communicate | Most common, local dev | +| **host** | Container shares host network | Performance critical apps | +| **none** | No network | Isolated containers | + +**Key Rule:** Containers on same network talk using **service names** not `localhost`! +```yaml +DB_HOST: mysql # βœ… service name +DB_HOST: localhost # ❌ looks inside itself! +``` + +--- + +## πŸ’Ύ Docker Volumes + +| Type | Description | Example | +|---|---|---| +| **Named Volume** | Managed by Docker, persists data | `mysql-vol:/var/lib/mysql` | +| **Bind Mount** | Maps host path to container path | `./init.sql:/docker-entrypoint-initdb.d/init.sql` | + +**When to use volumes:** +- βœ… Databases (MySQL, MongoDB, PostgreSQL) +- ❌ Frontend (static files in image) +- ❌ Backend (stateless, data goes to DB) + +--- + +## πŸ₯ Healthcheck +```yaml +healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s +``` + +**Why?** `depends_on` alone only waits for container to START, not to be READY! + +--- + +## πŸ”‘ Key Concepts to Remember + +1. **Image vs Container** β†’ Image is blueprint, Container is running instance +2. **Layer Caching** β†’ Copy dependencies first, then code = faster builds +3. **Multi-stage** β†’ Build in big image, serve in tiny image = smaller final image +4. **DB_HOST = service name** β†’ Each container has its own localhost! +5. **Volumes** β†’ Only for services that write data (databases) +6. **Healthcheck** β†’ Ensures service is READY, not just started +7. **Bridge Network** β†’ Containers communicate using service names as DNS + +--- + +## πŸš€ Quick Reference - Todo App +```bash +docker build -t todo-backend ./backend +docker build -t todo-frontend ./frontend +docker-compose up -d +docker ps +docker-compose logs backend +docker-compose down -v +docker rmi $(docker images -q) -f +``` + +--- + +*Made with ❀️ during 90DaysOfDevOps Challenge* +*GitHub: [@AddyKhan257](https://github.com/AddyKhan257)* diff --git a/2026/day-38/day-38-yaml.md b/2026/day-38/day-38-yaml.md new file mode 100644 index 0000000000..9cfe20dac8 --- /dev/null +++ b/2026/day-38/day-38-yaml.md @@ -0,0 +1,171 @@ +# πŸ“„ Day 38 – YAML Basics +> By Mohammad Adnan Khan | 90DaysOfDevOps + +--- + +## πŸ“˜ What is YAML? + +YAML stands for **"YAML Ain't Markup Language"**. +It is a human-readable data serialization language used to write configuration files. +Every CI/CD pipeline, Docker Compose file, and Kubernetes manifest is written in YAML. + +--- + +## βœ… Task 1: Key-Value Pairs + +**person.yaml:** +```yaml +name: Mohammad Adnan Khan +role: Aspiring DevOps Engineer +experience_years: 0 +learning: true +``` + +**What I learned:** +- YAML key-value pairs are written as `key: value` +- Booleans are written as `true` or `false` (no quotes) +- Strings don't need quotes unless they contain special characters + +--- + +## βœ… Task 2: Lists + +**Updated person.yaml:** +```yaml +name: Mohammad Adnan Khan +role: Aspiring DevOps Engineer +experience_years: 0 +learning: true + +tools: + - Docker + - Kubernetes + - Git + - Linux + - AWS + +hobbies: [coding, learning, cricket] +``` + +**Two ways to write a list in YAML:** +1. **Block style** β€” each item on new line with `-` (like `tools`) +2. **Inline style** β€” comma separated in `[]` (like `hobbies`) + +--- + +## βœ… Task 3: Nested Objects + +**server.yaml:** +```yaml +server: + name: prod-server-01 + ip: 192.168.1.10 + port: 8080 + +database: + host: db.internal + name: tododb + credentials: + user: root + password: Test@123 +``` + +**What I learned:** +- Nested objects use indentation (2 spaces per level) +- Never use tabs β€” only spaces! +- When I added a tab instead of spaces, yamllint showed: + `found character '\t' that cannot start any token` + +--- + +## βœ… Task 4: Multi-line Strings + +**Updated server.yaml:** +```yaml +server: + name: prod-server-01 + ip: 192.168.1.10 + port: 8080 + +database: + host: db.internal + name: tododb + credentials: + user: root + password: Test@123 + +startup_script: | + echo "Starting server..." + cd /app + npm install + node index.js + +shutdown_script: > + echo "Stopping server..." + cd /app + pm2 stop all +``` + +**When to use `|` vs `>`:** +- **`|` (block style)** β†’ preserves newlines β€” use for scripts, code, logs where line breaks matter +- **`>` (fold style)** β†’ folds into one line β€” use for long descriptions where line breaks don't matter + +--- + +## βœ… Task 5: Validate Your YAML + +- Validated `person.yaml` on yamllint.com βœ… +- Validated `server.yaml` on yamllint.com βœ… +- Intentionally broke indentation β€” got error: + `mapping values are not allowed here` +- Fixed it and validated again βœ… + +--- + +## βœ… Task 6: Spot the Difference +```yaml +# Block 1 - correct +name: devops +tools: + - docker + - kubernetes +``` +```yaml +# Block 2 - broken +name: devops +tools: +- docker + - kubernetes +``` + +**What's wrong with Block 2:** +- `- docker` has no indentation under `tools` β€” should be 2 spaces in +- `- kubernetes` has 2 spaces but `- docker` has 0 spaces β€” inconsistent indentation +- YAML expects all list items at the same indent level + +**Correct version:** +```yaml +name: devops +tools: + - docker + - kubernetes +``` + +--- + +## 🎯 3 Key Learnings from Day 38 + +1. **Spaces only, never tabs** β€” YAML will throw an error if you use tabs. Always use 2 spaces for indentation. +2. **Indentation is everything** β€” wrong indentation completely breaks the file. yamllint.com is your best friend! +3. **`|` vs `>`** β€” Use `|` when line breaks matter (scripts), use `>` when you want everything on one line (descriptions). + +--- + +## πŸ”— Files Created +- `person.yaml` β€” key-value pairs, lists +- `server.yaml` β€” nested objects, multi-line strings + +--- + +*Day 38 of 90DaysOfDevOps Challenge* +*#90DaysOfDevOps #DevOpsKaJosh #TrainWithShubham* diff --git a/2026/day-38/person.yaml b/2026/day-38/person.yaml new file mode 100644 index 0000000000..be00619190 --- /dev/null +++ b/2026/day-38/person.yaml @@ -0,0 +1,13 @@ +name: Mohammad Adnan Khan +role: Aspiring DevOps Engineer +experience_years: 0 +learning: true + +tools: + - Docker + - Kubernetes + - Git + - Linux + - AWS + +hobbies: [coding, learning, cricket] diff --git a/2026/day-38/server.yaml b/2026/day-38/server.yaml new file mode 100644 index 0000000000..291d206a54 --- /dev/null +++ b/2026/day-38/server.yaml @@ -0,0 +1,22 @@ +server: + name: prod-server-01 + ip: 192.168.1.10 + port: 8080 + +database: + host: db.internal + name: tododb + credentials: + user: root + password: Test@123 + +startup_script: | + echo "Starting server..." + cd /app + npm install + node index.js + +shutdown_script: > + echo "Stopping server..." + cd /app + pm2 stop all diff --git a/2026/day-39/day-39-cicd-concepts.md b/2026/day-39/day-39-cicd-concepts.md new file mode 100644 index 0000000000..05efbcab95 --- /dev/null +++ b/2026/day-39/day-39-cicd-concepts.md @@ -0,0 +1,145 @@ +# πŸš€ Day 39 – What is CI/CD? +> By Mohammad Adnan Khan | 90DaysOfDevOps + +--- + +## πŸ“Œ Task 1: The Problem With Manual Deployments + +### What can go wrong with 5 developers manually deploying? +- ❌ Two developers push conflicting code at the same time β†’ app breaks completely +- ❌ Someone forgets to test before deploying β†’ bugs go directly to production +- ❌ Code works on developer laptop but fails on the production server +- ❌ No rollback plan if deployment fails β†’ website goes down for hours +- ❌ No one knows whose code caused the bug β†’ blame game starts! +- ❌ Deployment takes hours β†’ slow delivery to customers + +### "It works on my machine" β€” Why is this a real problem? +Every developer has a different environment on their laptop β€” different OS version, different Node.js version, different environment variables, different installed packages. Code that runs perfectly locally can completely fail on the production server because the environments are different. + +Docker solved this partially by packaging the app with its environment. CI/CD solves it completely by always running code in a standardized, controlled environment β€” the same every single time, for every developer. + +### How many times can a team safely deploy manually? +| Manual Deployments | With CI/CD | +|---|---| +| 1-2 times per day maximum | 50-100+ times per day! | +| Slow, risky, error-prone | Fast, safe, automated | +| Hours to deploy | Minutes to deploy | + +--- + +## πŸ“Œ Task 2: CI vs CD vs CD + +### βœ… Continuous Integration (CI) +Developers merge code to a shared repo frequently (multiple times per day). Each merge automatically triggers a build and test process. CI catches integration bugs early β€” before they become big problems. + +**What it does:** Builds code β†’ Runs unit tests β†’ Reports pass/fail + +**Real-world example:** Netflix engineers push code 100+ times per day. Every push automatically runs thousands of tests within minutes. + +--- + +### 🚚 Continuous Delivery (CD) +After CI passes, the code is automatically prepared and packaged for deployment. The app is always in a deployable state. However, the actual deployment to production requires a **manual approval** button press. + +**What it does:** CI passes β†’ Build artifact β†’ Deploy to staging β†’ Ready for production + +**Real-world example:** A company runs CI/CD where every feature is tested and staged automatically, but a product manager clicks "Deploy to Production" manually for business reasons. + +--- + +### πŸ€– Continuous Deployment (CD) +Every change that passes all automated tests is automatically deployed to production β€” NO human approval needed. This is the most advanced stage. Only companies with very high test coverage and confidence use this. + +**What it does:** CI passes β†’ Deploy to staging β†’ Auto-deploy to production + +**Real-world example:** Amazon deploys to production every 11.7 seconds β€” fully automated! + +--- + +### Quick Comparison + +| Feature | CI | Continuous Delivery | Continuous Deployment | +|---|---|---|---| +| Auto Build | βœ… Yes | βœ… Yes | βœ… Yes | +| Auto Test | βœ… Yes | βœ… Yes | βœ… Yes | +| Auto Stage | ❌ No | βœ… Yes | βœ… Yes | +| Auto Prod Deploy | ❌ No | ❌ No (manual) | βœ… Yes (auto) | +| Human Approval | Build only | For production | Not needed | + +--- + +## πŸ“Œ Task 3: Pipeline Anatomy + +| Component | Definition | Example | +|---|---|---| +| 🎯 **Trigger** | The event that starts the pipeline automatically | git push, pull request, schedule | +| πŸ“¦ **Stage** | A logical phase/group in the pipeline | Build, Test, Deploy | +| βš™οΈ **Job** | A unit of work inside a stage β€” runs on a runner | run-unit-tests, build-docker | +| πŸ“ **Step** | A single command or action inside a job | npm install, npm test | +| πŸ–₯️ **Runner** | The machine/server that executes the job | ubuntu-latest, self-hosted | +| πŸ—‚οΈ **Artifact** | Output produced by a job β€” passed to next stage | app.jar, docker image, test-report | + +--- + +## πŸ“Œ Task 4: CI/CD Pipeline Diagram + +**Scenario:** Developer pushes code to GitHub β†’ App is tested β†’ Built into Docker image β†’ Deployed to staging server +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ πŸ”€ TRIGGER │────▢│ πŸ”¨ BUILD │────▢│ πŸ§ͺ TEST │────▢│ 🐳 DOCKER │────▢│ πŸš€ DEPLOY β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ git push β”‚ β”‚ npm install β”‚ β”‚ npm test β”‚ β”‚docker build β”‚ β”‚ SSH server β”‚ +β”‚ to GitHub β”‚ β”‚ npm build β”‚ β”‚ coverage β”‚ β”‚docker push β”‚ β”‚ docker up β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Detailed Breakdown + +| Stage | Job | Steps | Status | +|---|---|---|---| +| πŸ”€ Trigger | on: push to main | GitHub detects git push, starts pipeline | ⚑ Auto | +| πŸ”¨ Build | install-and-build | checkout code, npm install, npm run build | βœ… Pass/Fail | +| πŸ§ͺ Test | run-tests | npm test, generate coverage, report results | βœ… Pass/Fail | +| 🐳 Docker | build-and-push | docker build, docker tag, docker push to Hub | βœ… Pass/Fail | +| πŸš€ Deploy | deploy-staging | SSH to server, docker pull, docker-compose up | πŸŽ‰ Live! | + +--- + +## πŸ“Œ Task 5: Explore in the Wild β€” FastAPI + +**Repo explored:** `tiangolo/fastapi` on GitHub +**Workflow file:** `.github/workflows/test.yml` + +| Question | Answer | +|---|---| +| What triggers it? | push and pull_request events on main branch | +| How many jobs? | Multiple β€” lint, test (matrix: Python 3.8, 3.9, 3.10, 3.11), coverage | +| What does it do? | Installs FastAPI dependencies, runs pytest across multiple Python versions simultaneously using matrix strategy, uploads coverage to Codecov | +| Key learning | Matrix strategy runs same tests across multiple Python versions in parallel β€” saves time and ensures compatibility! | + +--- + +## 🎯 Key Learnings from Day 39 + +1. **CI/CD is a practice, not a tool** β€” GitHub Actions, Jenkins, GitLab CI are tools that implement CI/CD. The practice is about automating build, test, and deploy. +2. **A failing pipeline is a good thing!** β€” CI/CD catching a bug before production is exactly what it is designed to do. +3. **CI vs Delivery vs Deployment** β€” CI = auto test. Delivery = auto stage + manual prod. Deployment = fully automatic. +4. **Pipeline anatomy** β€” Trigger β†’ Stage β†’ Job β†’ Step β†’ Runner β†’ Artifact. +5. **Real teams deploy frequently** β€” Amazon: every 11.7 seconds. Netflix: 100s of times per day. Only possible with CI/CD! + +--- + +## πŸ”§ Popular CI/CD Tools + +| Tool | Best For | Key Feature | +|---|---|---| +| GitHub Actions | GitHub repos, open source | Built into GitHub, free for public repos | +| Jenkins | Enterprise, self-hosted | Most flexible, 1800+ plugins | +| GitLab CI | GitLab users | All-in-one DevOps platform | +| CircleCI | Fast builds | Very fast, great Docker support | +| ArgoCD | Kubernetes deployments | GitOps approach, K8s native | + +--- + +*Day 39 of 90DaysOfDevOps Challenge* +*#90DaysOfDevOps #DevOpsKaJosh #TrainWithShubham* diff --git a/2026/day-40/README.md b/2026/day-40/README.md index acc6eaf44b..ff88f733bd 100644 --- a/2026/day-40/README.md +++ b/2026/day-40/README.md @@ -98,5 +98,5 @@ Share your first green pipeline screenshot on LinkedIn. That green checkmark hit `#90DaysOfDevOps` `#DevOpsKaJosh` `#TrainWithShubham` -Happy Learning! +Happy learning! **TrainWithShubham** diff --git a/2026/day-40/day-40-first-workflow.md b/2026/day-40/day-40-first-workflow.md new file mode 100644 index 0000000000..eae520b848 --- /dev/null +++ b/2026/day-40/day-40-first-workflow.md @@ -0,0 +1,121 @@ +# βš™οΈ Day 40 – My First GitHub Actions Workflow +> By Mohammad Adnan Khan | 90DaysOfDevOps + +--- + +## πŸ“Œ Task 1: Setup + +- Created public repo: `github-actions-practice` on GitHub +- Cloned it locally +- Created folder structure: `.github/workflows/` + +--- + +## πŸ“Œ Task 2: Hello Workflow + +**File:** `.github/workflows/hello.yml` +```yaml +name: Hello GitHub Actions + +on: + push: + +jobs: + greet: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Say Hello + run: echo "Hello from GitHub Actions!" +``` + +βœ… Pushed to GitHub β†’ went to Actions tab β†’ Pipeline ran green! + +--- + +## πŸ“Œ Task 3: Workflow Anatomy + +| Key | What it does | +|---|---| +| `on:` | Defines what event triggers the pipeline (push, pull_request, schedule) | +| `jobs:` | Contains all the jobs to run in the pipeline | +| `runs-on:` | Defines which machine/OS the job runs on (ubuntu-latest, windows-latest) | +| `steps:` | List of individual tasks to execute inside a job | +| `uses:` | Runs a pre-built action from GitHub Marketplace (e.g. actions/checkout) | +| `run:` | Executes a shell command directly on the runner | +| `name:` | A human-readable label for the step β€” shows in Actions tab | + +--- + +## πŸ“Œ Task 4: Updated Workflow With More Steps +```yaml +name: Hello GitHub Actions + +on: + push: + +jobs: + greet: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Say Hello + run: echo "Hello from GitHub Actions!" + + - name: Print current date and time + run: date + + - name: Print branch name + run: echo "Branch is ${{ github.ref_name }}" + + - name: List files in repo + run: ls -la + + - name: Print runner OS + run: echo "Runner OS is $RUNNER_OS" +``` + +βœ… Pushed again β†’ All steps ran successfully β†’ Green pipeline! + +--- + +## πŸ“Œ Task 5: Break It On Purpose + +Added a failing step: +```yaml + - name: This will fail + run: exit 1 +``` + +**What a failed pipeline looks like:** +- ❌ Red cross on the Actions tab +- The failed step shows in red with the error message +- All steps AFTER the failed step are skipped automatically +- GitHub sends an email notification about the failure +- Easy to read β€” click the failed step and see exact error output + +**Fixed by removing `exit 1` β†’ pushed again β†’ back to green βœ…** + +--- + +## 🎯 Key Learnings from Day 40 + +1. **Workflow files live in `.github/workflows/`** β€” any `.yml` file there is automatically picked up by GitHub Actions +2. **Every push triggers a new run** β€” you can watch it live in the Actions tab +3. **`uses:` vs `run:`** β€” `uses` runs pre-built actions, `run` executes shell commands +4. **GitHub built-in variables** β€” `${{ github.ref_name }}` gives branch name, `$RUNNER_OS` gives OS +5. **Failed pipelines are helpful** β€” they tell you exactly which step failed and why + +--- + +## πŸ”— Repository +- GitHub: [github-actions-practice](https://github.com/AddyKhan257/github-actions-practice) + +--- + +*Day 40 of 90DaysOfDevOps Challenge* +*#90DaysOfDevOps #DevOpsKaJosh #Trainwithshubham* diff --git a/2026/day-41/.github/workflows/manual.yml b/2026/day-41/.github/workflows/manual.yml new file mode 100644 index 0000000000..60b8eb4161 --- /dev/null +++ b/2026/day-41/.github/workflows/manual.yml @@ -0,0 +1,23 @@ +name: Manual Deploy + +on: + workflow_dispatch: + inputs: + environment: + description: 'Choose environment' + required: true + default: 'staging' + type: choice + options: + - staging + - production + +jobs: + manual: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Print environment + run: echo "Deploying to environment ${{ github.event.inputs.environment }}" diff --git a/2026/day-41/.github/workflows/matrix.yml b/2026/day-41/.github/workflows/matrix.yml new file mode 100644 index 0000000000..b720fee137 --- /dev/null +++ b/2026/day-41/.github/workflows/matrix.yml @@ -0,0 +1,25 @@ +name: Matrix Build + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + os: [ubuntu-latest, windows-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Print Python version + run: python --version diff --git a/2026/day-41/.github/workflows/pr-check.yml b/2026/day-41/.github/workflows/pr-check.yml new file mode 100644 index 0000000000..c0ed5a4672 --- /dev/null +++ b/2026/day-41/.github/workflows/pr-check.yml @@ -0,0 +1,16 @@ +name: PR Check + +on: + pull_request: + branches: [main] + types: [opened, synchronize] + +jobs: + pr-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Print branch name + run: echo "PR check running for branch ${{ github.head_ref }}" diff --git a/2026/day-41/.github/workflows/schedule.yml b/2026/day-41/.github/workflows/schedule.yml new file mode 100644 index 0000000000..0187b908b4 --- /dev/null +++ b/2026/day-41/.github/workflows/schedule.yml @@ -0,0 +1,22 @@ +name: Scheduled Job + +on: + schedule: + - cron: '0 0 * * *' # Every day at midnight UTC + push: + branches: [main] + +jobs: + scheduled: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Print schedule info + run: echo "Scheduled job running at $(date)" +``` + +**Cron expression for every Monday at 9 AM:** +``` +0 9 * * 1 diff --git a/2026/day-41/README.md b/2026/day-41/README.md index d7852edec4..2293229bc5 100644 --- a/2026/day-41/README.md +++ b/2026/day-41/README.md @@ -89,3 +89,4 @@ Share your matrix build screenshot β€” seeing multiple jobs run in parallel for Happy Learning! **TrainWithShubham** +Adnan khan diff --git a/2026/day-41/day-41-triggers.md b/2026/day-41/day-41-triggers.md new file mode 100644 index 0000000000..364c5a2cec --- /dev/null +++ b/2026/day-41/day-41-triggers.md @@ -0,0 +1,210 @@ +# βš™οΈ Day 41 – Triggers & Matrix Builds +> By Mohammad Adnan Khan | 90DaysOfDevOps + +--- + +## πŸ“Œ Task 1: PR Trigger + +**File:** `.github/workflows/pr-check.yml` +```yaml +name: PR Check + +on: + pull_request: + branches: [main] + types: [opened, synchronize] + +jobs: + pr-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Print branch name + run: echo "PR check running for branch ${{ github.head_ref }}" +``` + +**How it works:** +- Triggers when PR is opened or updated against `main` +- `synchronize` = new commits pushed to the PR branch +- `github.head_ref` = the branch name of the PR + +--- + +## πŸ“Œ Task 2: Scheduled Trigger + +**File:** `.github/workflows/schedule.yml` +```yaml +name: Scheduled Job + +on: + schedule: + - cron: '0 0 * * *' + push: + branches: [main] + +jobs: + scheduled: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Print schedule info + run: echo "Scheduled job running at $(date)" +``` + +**Cron expression for every Monday at 9 AM:** +``` +0 9 * * 1 +``` + +| Field | Value | Meaning | +|---|---|---| +| Minute | 0 | At minute 0 | +| Hour | 9 | At 9 AM UTC | +| Day | * | Any day | +| Month | * | Any month | +| Weekday | 1 | Monday | + +--- + +## πŸ“Œ Task 3: Manual Trigger + +**File:** `.github/workflows/manual.yml` +```yaml +name: Manual Deploy + +on: + workflow_dispatch: + inputs: + environment: + description: 'Choose environment' + required: true + default: 'staging' + type: choice + options: + - staging + - production + +jobs: + manual: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Print environment + run: echo "Deploying to ${{ github.event.inputs.environment }}" +``` + +**How to trigger manually:** +1. Go to GitHub repo β†’ Actions tab +2. Find "Manual Deploy" workflow +3. Click "Run workflow" +4. Select staging or production +5. Click green "Run workflow" button βœ… + +--- + +## πŸ“Œ Task 4: Matrix Builds + +**File:** `.github/workflows/matrix.yml` +```yaml +name: Matrix Build + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + os: [ubuntu-latest, windows-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Print Python version + run: python --version +``` + +**Total jobs = 3 versions Γ— 2 OS = 6 jobs in parallel!** πŸ”₯ + +--- + +## πŸ“Œ Task 5: Exclude & Fail-Fast + +**File:** `.github/workflows/matrix-exclude.yml` +```yaml +name: Matrix with Exclude + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12"] + os: [ubuntu-latest, windows-latest] + exclude: + - os: windows-latest + python-version: "3.10" + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Print version + run: python --version +``` + +**fail-fast: true vs false:** + +| | fail-fast: true (default) | fail-fast: false | +|---|---|---| +| One job fails | All other jobs cancelled | All jobs continue | +| Use when | Save time | See all results | + +**Total jobs after exclude = 6 - 1 = 5 jobs!** + +--- + +## 🎯 Key Learnings from Day 41 + +1. **push** β€” triggers on every git push to specified branch +2. **pull_request** β€” triggers when PR is opened or updated +3. **schedule** β€” triggers on cron schedule (like a cron job) +4. **workflow_dispatch** β€” manual trigger with optional inputs +5. **Matrix builds** β€” run same job across multiple environments in parallel +6. **fail-fast: false** β€” all jobs run even if one fails +7. **exclude** β€” skip specific combinations in matrix + +--- + +## πŸ”— References +- GitHub: [AddyKhan257/90DaysOfDevOps](https://github.com/AddyKhan257/90DaysOfDevOps) + +--- + +*Day 41 of 90DaysOfDevOps Challenge* +*#90DaysOfDevOps #DevOpsKaJosh #TrainWithShubham* diff --git a/2026/day-42/day-42-runners.md b/2026/day-42/day-42-runners.md new file mode 100644 index 0000000000..0c3ff65cb4 --- /dev/null +++ b/2026/day-42/day-42-runners.md @@ -0,0 +1,194 @@ +# πŸƒ Day 42 – Runners: GitHub-Hosted & Self-Hosted +> By Mohammad Adnan Khan | 90DaysOfDevOps + +--- + +## πŸ“Œ Task 1: GitHub-Hosted Runners + +**What is a GitHub-Hosted Runner?** +A GitHub-hosted runner is a virtual machine provided and managed by GitHub. You don't need to set it up β€” GitHub handles everything including OS, software, maintenance, and security. + +**Who manages it?** GitHub manages it completely β€” you just use it! + +```yaml +name: Multi-OS Jobs + +on: + push: + branches: [main] + +jobs: + ubuntu-job: + runs-on: ubuntu-latest + steps: + - name: Print OS info + run: | + echo "OS: Ubuntu" + echo "Hostname: $(hostname)" + echo "User: $(whoami)" + + windows-job: + runs-on: windows-latest + steps: + - name: Print OS info + run: | + echo "OS: Windows" + echo "Hostname: $env:COMPUTERNAME" + echo "User: $env:USERNAME" + + macos-job: + runs-on: macos-latest + steps: + - name: Print OS info + run: | + echo "OS: macOS" + echo "Hostname: $(hostname)" + echo "User: $(whoami)" +``` + +--- + +## πŸ“Œ Task 2: Pre-installed Software on ubuntu-latest + +```yaml +- name: Check pre-installed tools + run: | + echo "Docker: $(docker --version)" + echo "Python: $(python3 --version)" + echo "Node: $(node --version)" + echo "Git: $(git --version)" +``` + +**Why does it matter that runners come with tools pre-installed?** +- No need to install Docker, Python, Git on every run β†’ saves time +- Consistent environment for every pipeline run +- Focus on your workflow logic, not environment setup +- Free compute time not wasted on installations + +--- + +## πŸ“Œ Task 3: Self-Hosted Runner Setup βœ… + +**Steps followed:** +1. GitHub repo β†’ Settings β†’ Actions β†’ Runners β†’ New self-hosted runner +2. Selected: Linux, x64 +3. Downloaded and configured runner on AWS EC2 + +```bash +# Download runner +mkdir actions-runner && cd actions-runner +curl -o actions-runner-linux-x64-2.332.0.tar.gz -L \ + https://github.com/actions/runner/releases/download/v2.332.0/actions-runner-linux-x64-2.332.0.tar.gz + +# Verify checksum +echo "f2094522a6b9afeab07ffb586d1eb3f190b6457074282796c497ce7dce9e0f2a actions-runner-linux-x64-2.332.0.tar.gz" | shasum -a 256 -c + +# Extract +tar xzf ./actions-runner-linux-x64-2.332.0.tar.gz + +# Configure +./config.sh --url https://github.com/AddyKhan257/Git-Hub-Action-Zero-To-Hero \ + --token YOUR_TOKEN + +# Start runner +./run.sh +``` + +**Runner registered as:** `khanshab` +**Labels:** `self-hosted`, `Linux`, `X64`, `prod` +**Status:** βœ… Idle β€” showing green in GitHub + +--- + +## πŸ“Œ Task 4: Workflow Using Self-Hosted Runner + +```yaml +name: Self-Hosted Job + +on: + workflow_dispatch: + +jobs: + run-on-my-machine: + runs-on: self-hosted + steps: + - name: Print hostname + run: echo "Running on $(hostname)" + + - name: Print working directory + run: pwd + + - name: Create a file + run: | + echo "Created by GitHub Actions on $(date)" > proof.txt + cat proof.txt + + - name: Verify file exists + run: ls -la proof.txt +``` + +**Result:** File `proof.txt` was created on my EC2 server! βœ… +**Hostname showed:** `ip-172-31-35-160` β€” my actual AWS EC2 machine! + +--- + +## πŸ“Œ Task 5: Runner Labels + +**Label added:** `prod` + +```yaml +# Target specific runner using labels +jobs: + deploy: + runs-on: [self-hosted, Linux, prod] + steps: + - run: echo "Running on prod runner!" +``` + +**Why are labels useful when you have multiple runners?** +- You might have `dev`, `staging`, `prod` runners +- Labels let you target the right machine for the right job +- Example: deploy to prod only runs on the `prod` labelled runner +- Prevents accidentally running prod code on dev machines + +--- + +## πŸ“Œ Task 6: GitHub-Hosted vs Self-Hosted + +| | GitHub-Hosted | Self-Hosted | +|---|---|---| +| **Who manages it?** | GitHub manages everything | You manage the server | +| **Cost** | Free (with limits) | You pay for the server | +| **Pre-installed tools** | Docker, Python, Node, Git etc. | You install what you need | +| **Good for** | Open source, quick pipelines | Private repos, custom hardware | +| **Security concern** | Code runs on GitHub's servers | Code runs on YOUR server β€” more control | +| **Persistence** | Fresh VM every run | Files persist between runs | +| **Speed** | Depends on GitHub queue | Depends on your server specs | + +--- + +## 🎯 Key Learnings from Day 42 + +1. **GitHub-hosted runners** = Virtual machines managed by GitHub, free with limits +2. **Self-hosted runners** = Your own server registered to GitHub +3. `runs-on: ubuntu-latest` = GitHub's server +4. `runs-on: self-hosted` = YOUR server +5. **Labels** help target specific runners in multi-runner setups +6. Self-hosted runners are perfect for production deployments +7. `./run.sh` starts the runner, `./svc.sh install` makes it a permanent service + +--- + +## πŸ“Έ Screenshots +- βœ… Self-hosted runner registered (runner config in terminal) +- βœ… Runner showing as Idle in GitHub (khanshab β€” green dot) + +--- + +## πŸ”— References +- GitHub: [AddyKhan257/90DaysOfDevOps](https://github.com/AddyKhan257/90DaysOfDevOps/tree/master/2026/day-42) + +--- + +*Day 42 of 90DaysOfDevOps Challenge* +*#90DaysOfDevOps #DevOpsKaJosh #TrainWithShubham*