Transforming DevOps: Building a Production-Ready CI/CD Pipeline
A hands-on story of breaking things, learning, and automating a React app deployment to AWS
I built a fully automated CI/CD pipeline that takes a React application from code commit to production deployment on AWS in under 10 minutes. Along the way, I learned Jenkins, Docker, Terraform, Ansible, and AWS , while debugging real-world issues that taught me more than any tutorial ever could.
Tech Stack: Jenkins | Docker | Terraform | Ansible | AWS | React | Node.js
GitHub: https://github.com/akanksha106-code/Zomato
The Challenge: Manual Deployments Are Painful
Picture this: It's 2 AM. Your website is down. You need to deploy a fix. You scramble through a 20-step deployment document, praying you don't miss anything. Sound familiar?
This is exactly the problem DevOps automation solves. But as a developer transitioning into DevOps, I didn't just want to read about it—I wanted to build it.
So I set out to create a real CI/CD pipeline. Not a toy project. Not a "hello world" deployment. A production-pattern system that could actually be used in a real company.
Here's what I learned.
Part 1: The Vision
What I Wanted to Build
A system where:
A developer pushes code to GitHub
Magic happens automatically
The app deploys to AWS
Zero manual steps
Complete traceability
The Architecture
Developer (Git Push)
↓
GitHub Repository
↓
Jenkins CI/CD Server
↓
[Clean → Build → Push → Provision → Deploy]
↓
AWS EC2 Running App
↓
Users Access Application
Simple in theory. But as I'd soon learn, the devil is in the details.
Part 2: The Setup
The Infrastructure
I started with a Jenkins server on AWS:
Instance Type: t2.medium (2 vCPU, 4GB RAM)
OS: Amazon Linux 2
Tools Installed: Java 21, Node.js 18, Docker, Terraform, Ansible, Git
The Application
A React-based food delivery app (Zomato clone):
Built with React 18.2.0
Material-UI for components
Dockerized for consistency
Runs on Node.js 16
The Pipeline Stages
I designed a 6-stage pipeline:
Clean Workspace - Start fresh every time
Git Checkout - Pull latest code
Build Docker Image - Package the app
Push to DockerHub - Store the artifact
Terraform Provision - Create AWS infrastructure
Ansible Deploy - Configure and deploy
Sounds straightforward, right? That's what I thought too.
Part 3: The Breaks
Problem #1: The Docker Tag Mystery
What Happened:
Stage 1-4: All green
Stage 6: Error pulling image akankshatech/zomatoapp:1.0 - 404 Not Found
My Initial Reaction: "But it just built successfully! I can see it in DockerHub!"
The Investigation: I logged into DockerHub and saw the image... tagged as latest, not 1.0.
Checked my Jenkinsfile:
sh "docker build -t akankshatech/zomatoapp:latest ."
Checked my Ansible playbook:
image: akankshatech/zomatoapp:1.0 # ← There's the problem!
The Learning: Configuration consistency matters. Every component must agree on tagging strategies. In production, I'd use semantic versioning or git commit SHAs for immutable tags.
The Fix:
# Updated Ansible playbook
image: akankshatech/zomatoapp:latest
Time Lost: 2 hours | Value Gained: Understanding of container registries and tagging strategies
Problem #2: Permission Denied
What Happened:
permission denied while trying to connect to the Docker daemon socket
unix:///var/run/docker.sock
The Confusion: I could run docker ps manually as the jenkins user. It worked perfectly. But in the pipeline? Permission denied.
The Investigation:
# Check groups
$ groups jenkins
jenkins : jenkins docker # ← jenkins IS in docker group!
# Check socket permissions
$ ls -la /var/run/docker.sock
srw-rw---- 1 root docker # ← docker group has access!
# So why doesn't it work?
The "Aha!" Moment: Group membership is assigned at login time. I had added jenkins to the docker group while Jenkins was already running. The running process didn't have the new group membership!
The Fix:
sudo usermod -aG docker jenkins
sudo systemctl restart jenkins # ← This was the key!
The Learning: Linux group mechanics aren't dynamic. Processes inherit group membership at startup. This is why configuration management tools restart services after group changes.
Time Lost: 3 hours of frustrated Googling | Value Gained: Deep understanding of Linux permissions and process management
Problem #3: The Sudo Password Puzzle
What Happened:
sudo: a terminal is required to read the password
sudo: a password is required
Why This Was Confusing: Ansible has become: true in the playbook. SSH key authentication works. Why is it asking for a password?
The Understanding:
SSH key authentication ≠ Sudo authentication
SSH lets you connect to the server
Sudo is a separate authorization layer
Ansible can't provide a password in automated pipelines (no interactive terminal)
The Fix:
$ sudo visudo
# Added:
jenkins ALL=(ALL) NOPASSWD:ALL
The Learning: In production, this is a security risk. Better approaches:
Limited sudo permissions for specific commands only
Use AWS Systems Manager Session Manager instead of SSH
Container-based deployments that don't need sudo
Time Lost: 1 hour | Value Gained: Understanding authentication vs. authorization
Problem #4: The Port Mapping Mixup
What Happened: App deployed successfully, but accessing http://server-ip:8084 showed nothing.
The Investigation:
# Check container
$ docker ps
CONTAINER STATUS PORTS
zomatoapp Up 0.0.0.0:8084->8081/tcp # ← Wait, what?
# Check Dockerfile
EXPOSE 3000 # ← App listens on 3000, not 8081!
The Root Cause: I had copied the Ansible playbook from a different tutorial where the app used port 8081. My app uses 3000.
The Fix:
# Updated Ansible playbook
published_ports:
- "8084:3000" # Changed from 8084:8081
The Learning: Don't blindly copy-paste configuration. Understand what each parameter does. Port mapping is HOST:CONTAINER, not the other way around.
Part 4: The Build
After fixing these issues (and a few more I've spared you), I finally had a working pipeline.
The Pipeline in Action
Trigger: Push code to GitHub
[STAGE 1] Clean Workspace ..................... 5s
[STAGE 2] Git Checkout ........................ 8s
[STAGE 3] Build Docker Image .................. 156s
[STAGE 4] Push to DockerHub ................... 45s
[STAGE 5] Terraform Provision AWS ............. 98s
[STAGE 6] Ansible Configure & Deploy .......... 134s
Total Time: 7 minutes 26 seconds
Status: SUCCESS
What Actually Happens
Stage 3: Build Docker Image
FROM node:16-slim
WORKDIR /app
COPY package*.json ./
RUN npm install # ← Cached if packages unchanged!
COPY . .
RUN npm run build # ← Creates production bundle
EXPOSE 3000
CMD ["npm", "start"]
Docker's layer caching is brilliant:
First build: 3 minutes
Subsequent builds (code changed, dependencies same): 45 seconds
80% faster!
Stage 5: Terraform Provision
resource "aws_instance" "test-server" {
ami = "ami-0532be01f26a3de55" # Amazon Linux 2
instance_type = "t3.small" # 2 vCPU, 2GB RAM
# Creates server, waits for SSH, generates inventory
provisioner "local-exec" {
command = "echo '${self.public_ip}' > ./inventory"
}
}
Terraform is declarative magic:
Reads configuration files
Compares to current AWS state
Creates only what's needed
Tracks everything in state file
Stage 6: Ansible Deploy
tasks:
- name: Update packages
yum: name="*" state=latest
- name: Install Docker
yum: name=docker state=present
- name: Start Docker
systemd: name=docker state=started enabled=yes
- name: Deploy container
docker_container:
name: zomatoapp
image: akankshatech/zomatoapp:latest
state: started
restart_policy: always
published_ports:
- "8084:3000"
Idempotency in action:
Run once: Installs everything
Run again: "Already done, skipping"
Run 100 times: Same result
Part 5: The Result
Container Status
$ docker ps
CONTAINER ID IMAGE STATUS PORTS
a1b2c3d4e5f6 akankshatech/zomatoapp:latest Up 3 hours 0.0.0.0:8084->3000/tcp
Metrics That Matter
| Metric | Before Automation | After Automation |
| Deployment Time | 45-60 minutes | 8-10 minutes |
| Manual Steps | 23 steps | 0 steps (1 git push) |
| Error Rate | ~30% (human error) | <5% (mostly code bugs) |
| Rollback Time | 60+ minutes | 5 minutes (redeploy previous version) |
| Environment Consistency | "Works on my machine" | Identical everywhere |
| Documentation | 15-page Word doc | Git repository (self-documenting) |
Part 6: The Learnings
Technical Skills Gained
1. Jenkins Administration
- Plugin management, credential storage, pipeline scripting (Groovy), tool configuration
2. Docker Mastery
- Multi-stage builds, layer caching optimization, port mapping, container orchestration basics
3. Terraform (Infrastructure as Code)
- Resource management, state handling, provisioners, AWS provider
4. Ansible (Configuration Management)
- Playbook writing, idempotency, module usage, SSH connection management
5. AWS Cloud Services
- EC2 instance management, security groups, IAM roles and policies, key pair management
6. Linux System Administration
- User and group management, systemd service control, package management (yum), file permissions
Problem-Solving Skills
Before this project: I'd Google an error and copy-paste the first solution.
After this project: I now:
Read the full error message
Understand what the system is trying to tell me
Form a hypothesis
Test systematically
Understand the root cause
Implement a proper fix
Document what I learned
This is the real value.
Part 7: The Lessons
Lesson 1: Tutorials Teach Syntax, Problems Teach Concepts
The tutorial showed me the commands. The errors taught me:
How Docker layers work
Why Linux groups exist
What Terraform state does
How Ansible achieves idempotency
The problems were the best teachers.
Lesson 2: Production Is Different
This project works for learning. Production needs:
High availability
Security hardening
Monitoring and alerting
Disaster recovery
Cost optimization
Compliance requirements
Knowing the gap is as important as building the project.
Lesson 3: DevOps Is More Than Tools
It's about:
Culture: Breaking silos between dev and ops
Automation: Eliminating manual toil
Measurement: You can't improve what you don't measure
Sharing: Knowledge sharing and collaboration
The tools are just enablers.
Lesson 4: Documentation Matters
When I hit an error at 11 PM, good documentation saved me hours. I learned to:
Write clear error messages
Document configuration decisions
Keep a troubleshooting log
Share knowledge with others
This blog post is part of that.
Lesson 5: Learning Never Stops
After completing this project, I realized how much more there is:
Kubernetes and container orchestration
Service mesh (Istio, Linkerd)
GitOps (ArgoCD, Flux)
Observability (Prometheus, Grafana, Jaeger)
Chaos engineering
FinOps (cloud cost optimization)
DevOps is a journey, not a destination.
Key Takeaways
This project transformed me from someone who reads about DevOps to someone who does DevOps.
I can now:
Build CI/CD pipelines
Automate infrastructure
Debug complex systems
Make architectural decisions
Speak confidently about these tools in interviews
This is portfolio-worthy work.
Technical Appendix
Repository Structure
Zomato/
├── src/ # React source code
├── public/ # Static assets
├── terraform-files/
│ ├── main.tf # Infrastructure definition
│ ├── provider.tf # AWS provider
│ ├── ansible.cfg # Ansible config
│ ├── ansiblebook.yml # Deployment playbook
│ └── inventory # Generated dynamically
├── Dockerfile # Container definition
├── Jenkinsfile1 # Pipeline definition
├── package.json # Dependencies
└── README.md # Documentation
Key Configuration Files
Dockerfile:
FROM node:16-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Jenkinsfile (Simplified):
pipeline {
agent any
stages {
stage("Clean") { steps { cleanWs() } }
stage("Checkout") { steps { git 'repo-url' } }
stage("Build") { steps { sh 'docker build -t app .' } }
stage("Push") { steps { sh 'docker push app' } }
stage("Provision") { steps { sh 'terraform apply' } }
stage("Deploy") { steps { ansiblePlaybook(...) } }
}
}
Let's Connect!
I'm actively looking for DevOps opportunities where I can build and improve CI/CD pipelines, work with cloud infrastructure, and learn from experienced engineers.
Find me: