DEV Community

Alan Varghese
Alan Varghese

Posted on

From Docker Errors to Production-Ready: Building a PHP Microservices CI/CD Pipeline

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")
*

  1. 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.

  1. 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
  1. 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.

  1. 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
Enter fullscreen mode Exit fullscreen mode

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)

  1. Infrastructure as Code ≠ Magic

It requires the same discipline as application code:

Version control everything
Write documentation
Test changes
Review before applying

  1. 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)

  1. 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

  1. 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)