Introduction
Ever felt like Docker Compose was conspiring against you? Or that GitHub Actions had a personal vendetta? I've been there. This is the story of how I turned a tangled mess of microservices, port conflicts, and CI/CD failures into a fully automated, production-ready pipeline.
What started as a simple PHP project evolved into a deep dive into DevOps practices, container orchestration, and automation. Here's my journey, complete with code snippets, painful lessons, and hard-won victories.
The Project: PHP Microservices Architecture
Goal: Build a scalable PHP microservices application with complete CI/CD automation.
Stack:
PHP 8.2 with FPM for backend services
Nginx as reverse proxy and load balancer
MySQL 8.0 and Redis for data layer
Docker Compose for orchestration
GitHub Actions for CI/CD
Prometheus + Grafana for monitoring
Port configuration: 9000, 9001, 8081, 3000, 9090
Architecture:
Client → Nginx (8081) → API Gateway → Microservices
↓
User Service (9000)
Product Service
Order Service
↓
Monitoring (9001, 9090, 3000)
*The Challenges (Aka "What Broke First")
*
- Docker Compose YAML Syntax Hell
The error that started it all
Error: services must be a mapping
Lesson learned: YAML is space-sensitive poetry. Two spaces, not tabs. Always.
- The Port Conflict Tango
My services kept fighting over ports. The solution? Document everything:
ports:
- "9000:9000" # PHP Application
- "9001:9001" # Monitoring Dashboard
- "8081:8081" # API Gateway
- "3000:3000" # Frontend Dev Server
- "9090:9090" # Metrics Collector
- GitHub Actions: The .env.example Ghost File
The most frustrating error:
cp: cannot stat '.env.example': No such file or directory
Turns out, the file existed locally but wasn't tracked by Git. Classic.
- Mac vs Linux: The Line Ending Wars
What my Mac created
file .env.example
.env.example: ASCII text, with CRLF line terminators
What GitHub Actions (Linux) expected
.env.example: ASCII text
Solution:
Convert CRLF to LF
sed -i '' 's/\r$//' .env.example
🔧 The Solutions: Building Resilience
Docker Compose Done Right
After several iterations, here's the working structure:
version: '3.8'
services:
php-app:
build: ./api
container_name: php-microservice-app
ports:
- "${API_PORT:-9000}:9000"
environment:
- DB_HOST=${DB_HOST}
- DB_PASSWORD=${DB_PASSWORD}
volumes:
- ./api/src:/var/www/html
networks:
- app-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/health"]
interval: 30s
timeout: 10s
retries: 3
... other services
networks:
app-network:
driver: bridge
GitHub Actions Workflow That Actually Works
The key was creating a robust workflow that handles failures gracefully:
name: PHP Microservices CI/CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v3
- name: 📄 Create .env (with fallback)
run: |
Don't fail if .env.example doesn't exist
if [ -f ".env.example" ]; then
cp .env.example .env
else
echo "APP_ENV=ci" > .env
echo "DB_PASSWORD=ci_test_$(date +%s)" >> .env
fi
CI-specific overrides
echo "API_PORT=19000" >> .env
echo "NGINX_PORT=18081" >> .env
- name: 🐳 Build and Test
run: |
docker-compose build
docker-compose up -d
sleep 30
Health checks
curl -f http://localhost:19000/health || exit 1
docker-compose down
The Health Check Script
Because services need checkups too:
!/bin/bash
scripts/health-check.sh
echo "🔍 Running Microservices Health Check..."
echo "========================================"
check_service() {
local service=$1
local port=$2
local max_attempts=5
for i in $(seq 1 $max_attempts); do
if curl -s -f "http://localhost:$port" > /dev/null; then
echo "✅ $service (port $port): HEALTHY"
return 0
fi
echo "⏳ Attempt $i: $service not ready..."
sleep 5
done
echo "❌ $service FAILED after $max_attempts attempts"
return 1
}
check_service "PHP API" 9000
check_service "Monitoring" 9001
check_service "API Gateway" 8081
check_service "Frontend" 3000
check_service "Prometheus" 9090
** Monitoring Stack: Seeing Is Believing**
I set up a complete monitoring solution:
Prometheus (port 9090): Metrics collection
Grafana (port 9001): Visualization dashboards
Custom metrics: Service health, response times, error rates
Grafana Dashboard JSON snippet:
{
"dashboard": {
"title": "PHP Microservices Monitor",
"panels": [
{
"title": "Service Health",
"type": "stat",
"targets": [
{
"expr": "up{job='php-app'}",
"legendFormat": "{{instance}}"
}
]
}
]
}
}
Key Learnings (The Hard Way)
- Infrastructure as Code ≠ Magic
It requires the same discipline as application code:
Version control everything
Write documentation
Test changes
Review before applying
- CI/CD Is About Feedback Loops
Fast feedback prevents big failures. My workflow now includes:
Syntax validation (docker-compose config)
Build validation (docker-compose build)
Runtime validation (health checks)
Performance checks (response times)
- Ports Are Precious Real Estate
Document and reserve them early:
My port allocation table
9000 - Main application
9001 - Monitoring dashboard
8081 - API gateway
3000 - Development frontend
9090 - Metrics collector
- The 12-Factor App Applies to DevOps Too
III. Config: Store config in environment variables
IV. Backing services: Treat databases as attached resources
V. Build, release, run: Strict separation
XI. Logs: Treat logs as event streams
🚀 Production Deployment Strategy
After local success, I created a production deployment strategy:
!/bin/bash
scripts/deploy-production.sh
1. Pull latest images
docker-compose pull
2. Backup database
docker-compose exec -T mysql \
mysqldump -u root -p$DB_PASSWORD $DB_DATABASE > backup.sql
3. Update with zero downtime
docker-compose up -d --scale php-app=3 --no-recreate
4. Health check
./scripts/health-check.sh
5. Remove old containers
docker system prune -af
Resources That Saved Me
Docker Documentation - Surprisingly readable
GitHub Actions Docs - Examples galore
Prometheus Querying - Metrics mastery
man pages - Old school, but gold
This journey taught me that DevOps isn't about tools—it's about mindset. It's about building systems that are:
Observable (you can see what's happening)
Maintainable (you can fix what's broken)
Scalable (you can grow when needed)
Questions for the community:
How do you handle port conflicts in multi-service projects?
What's your favorite Docker Compose tip or trick?
How do you balance comprehensive monitoring with simplicity?
Project Repository: https://github.com/alanvarghese-dev/devops-ci-cd-pipeline
** About the Author**
I'm a developer on a DevOps journey, sharing my learnings as I go. Follow me here on DEV for more posts about containerization, automation, and building resilient systems.
Discussion: Have you faced similar challenges with Docker or CI/CD? Share your stories in the comments!
Like this post? Give it a ❤️ and share your own CI/CD war stories in the comments!
Top comments (0)