Simple DevOps Crash Course: Build a Real CI/CD Pipeline in 15 Minutes
DevOps sounds like a fancy corporate buzzword, but the idea is dead simple. Developers write code. Operations engineers keep servers running. In the old days, developers threw their code over a wall to the operations team and prayed that it worked. When it crashed, everyone blamed each other.
DevOps removes the wall. It turns manual tasks into code. Instead of copying files to a server late at night, you write scripts that test, build, and deploy your software automatically every time you push a change to Git. Here is your hands-on programming tutorial to set up a real pipeline from scratch.
The Core Workflow: Code, Test, Build, Ship
Every reliable DevOps setup follows a loop called CI/CD. CI stands for Continuous Integration. CD stands for Continuous Delivery or Continuous Deployment. Here is what happens in that loop:
- Continuous Integration: Whenever you push code, an automated server grabs it, installs dependencies, and runs your test suite. If a test fails, the pipeline stops and alerts you immediately.
- Continuous Delivery: Once tests pass, the pipeline packages your code into a clean, ready-to-run container or artifact.
- Continuous Deployment: The pipeline automatically pushes that container to your staging or production servers without any human button-clicking.
Step 1: Write a Simple App and a Test
Let us create a small Python web service. We will write one tiny function and one unit test so our pipeline has something real to verify.
Save this as app.py:
def calculate_discount(price, discount_percent):
if price < 0 or discount_percent < 0:
raise ValueError("Values cannot be negative")
if discount_percent > 100:
raise ValueError("Discount cannot exceed 100 percent")
return price * (1 - (discount_percent / 100))
if __name__ == "__main__":
print(f"Test price: {calculate_discount(100, 20)}")
Now create a test file named test_app.py using Python's built-in unittest runner:
import unittest
from app import calculate_discount
class TestApp(unittest.TestCase):
def test_standard_discount(self):
self.assertEqual(calculate_discount(100, 10), 90.0)
def test_negative_price(self):
with self.assertRaises(ValueError):
calculate_discount(-50, 10)
if __name__ == "__main__":
unittest.main()
Step 2: Automate Everything with GitHub Actions
You do not need to pay for heavy enterprise servers to learn DevOps. GitHub Actions gives you free runners. To create your first pipeline, make a folder in your project called .github/workflows and put a file named pipeline.yml inside it.
name: Main Pipeline
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Run unit tests
run: |
python -m unittest test_app.py
Every time you run git push, GitHub spins up a clean Ubuntu server, pulls your branch, and runs your tests. If someone writes bad code that breaks a calculation, the pull request turns red and blocks the merge.
Step 3: Package the App with Docker
The oldest problem in software development is the phrase: "It worked on my machine." A container solves this. It wraps your code, the exact operating system, and all packages into an immutable box.
Create a file named Dockerfile in your root folder:
# Use a tiny, secure base image
FROM python:3.11-slim
# Create a non-root system user for security
RUN useradd -m appuser
WORKDIR /home/appuser
# Copy app code
COPY app.py .
USER appuser
# Run the script
CMD ["python", "app.py"]
Build and run the container locally to verify it works:
docker build -t simple-devops-app:latest .
docker run --rm simple-devops-app:latest
Step 4: Handy Coding Shortcuts with Makefiles
Typing long Docker and Git commands over and over wastes time and invites typos. Veteran engineers use small coding shortcuts. A simple Makefile in your project root lets you run everything with short, memorable words.
.PHONY: test build run clean
test:
python3 -m unittest test_app.py
build:
docker build -t simple-devops-app:latest .
run:
docker run --rm simple-devops-app:latest
all: test build run
Now, instead of remembering three different shell commands, you just open your terminal and type make all. If tests fail, Make stops immediately and never builds the Docker image.
Edge Cases and Common DevOps Pitfalls
Pipelines break in funny ways when you move to real production systems. Watch out for these four common traps:
1. Unpinned Dependencies
If your build script installs packages using pip install flask without fixing the version number, your pipeline might pass today and break tomorrow when Flask releases a major update. Always lock your versions using a lockfile or exact tags like flask==3.0.2.
2. Hardcoded Secrets
Never put database passwords, SSH keys, or API tokens inside your Git repository or your Dockerfile. Anyone with read access can steal them. Use environment variables and store credentials inside GitHub Secrets or an encrypted vault.
3. Ignoring Exit Codes
In shell scripts, commands often fail quietly while the script keeps going. Always add set -e at the top of bash scripts used in your pipelines. This tells bash to halt execution immediately if any single line returns a non-zero exit code.
4. Storage Leaks on Self-Hosted Runners
If you run your own build agents, old Docker images and build caches will eat all your hard drive space within weeks. Set up automated cron jobs to run docker system prune -af --volumes on your build nodes weekly.
Summary Checklist for Your Daily Work
Good DevOps is not about using twenty different tools at once. It is about keeping your release cycle predictable and painless. Keep your automated steps small, fail fast when something is wrong, and automate any task you find yourself doing more than twice.
" ,Frequently Asked Questions
Q.
Q.
Q.
Hardeep Singh
Lead AI & Full-Stack Architect | Founder, Coding's Art
Hardeep is a senior software engineer and published author specializing in Python automation, enterprise Laravel systems, and autonomous AI agents. With over 8 years of hands-on experience, he designs robust digital solutions and publishes practical engineering playbooks for developers and technical leaders across India and globally.
Related Developer Guides
PostgreSQL and MySQL Indexing Secrets: Speed Up Slow Queries on 10-Million Row Tables
Speed up slow queries on 10-million row tables by creating composite indexes that follow the Equality-Sort-Range rule and cutting index size with partial indexing. These strategies replace heavy full table scans with instant index seeks to drop query latency from seconds down to milliseconds.
Cursor AI and GitHub Copilot Prompting Rules That Write Bug-Free Code on the First Try
To get bug-free code on the first try with Cursor AI and GitHub Copilot, feed them strict type definitions, narrow scopes, and test assertions before writing logic. Treating AI like a junior developer who needs exact guardrails stops hallucinations and syntax errors before they start. Here are the core prompting rules and system setups that work every time.
Building Resilient REST APIs in Laravel: 6 Idempotency and Rate-Limit Secrets
A resilient REST API handles network drops and double clicks without corrupting data or crashing servers. In Laravel, you achieve this by pairing atomic cache locks with custom idempotency keys and dynamic rate limiters. Here are six simple secrets to protect your endpoints right now.