• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Migrating Legacy PHP 7.x Applications to PHP 9 with Zero Downtime: A Deep Dive into Strangler Fig Pattern and Docker Orchestration

Migrating Legacy PHP 7.x Applications to PHP 9 with Zero Downtime: A Deep Dive into Strangler Fig Pattern and Docker Orchestration

Understanding the Strangler Fig Pattern for PHP Migrations

Migrating monolithic legacy PHP 7.x applications to a modern PHP 9 environment, especially with zero downtime requirements, is a complex undertaking. A common and highly effective architectural strategy for this is the Strangler Fig pattern. This pattern involves gradually replacing pieces of the legacy system with new services, routing traffic to the new components as they become ready. The legacy system, like a vine strangling a tree, is slowly but surely replaced by the new architecture.

For PHP applications, this often means identifying distinct functionalities or modules within the monolith and reimplementing them as microservices or distinct PHP 9 applications. The key is to have a facade or proxy layer that intelligently routes requests to either the legacy system or the new PHP 9 service based on the request’s nature.

Setting Up the Facade: Nginx as a Reverse Proxy

Nginx is an excellent choice for implementing the facade layer due to its high performance, robust feature set, and flexibility. We’ll configure Nginx to act as a reverse proxy, directing traffic. Initially, all traffic will go to the legacy PHP 7.x application. As we build and deploy new PHP 9 services, we’ll update the Nginx configuration to route specific requests to these new services.

Consider a legacy application with distinct API endpoints for `users` and `products`. We’ll start by routing all traffic to the existing PHP 7.x application. Then, we’ll build a new PHP 9 microservice for `users` and update Nginx to proxy requests to `/api/v1/users` to this new service.

Initial Nginx Configuration (All Traffic to Legacy PHP 7.x)

This configuration assumes your legacy PHP 7.x application is served by PHP-FPM on port 9000.

# /etc/nginx/sites-available/legacy_app.conf

server {
    listen 80;
    server_name yourdomain.com;
    root /var/www/legacy_app/public; # Path to your legacy app's public directory

    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        # Assuming PHP 7.x FPM is running on this socket/port
        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
        # Or if using TCP: fastcgi_pass 127.0.0.1:9000;
    }

    # Deny access to hidden files
    location ~ /\.ht {
        deny all;
    }
}

Containerizing the Legacy and New Applications with Docker

To facilitate gradual rollout and independent deployment, containerizing both the legacy PHP 7.x application and the new PHP 9 services is crucial. Docker provides the isolation and portability needed. We’ll use Docker Compose for local development and testing, and a more robust orchestrator like Kubernetes for production.

Dockerfile for Legacy PHP 7.x Application

This Dockerfile assumes your legacy application code is in a directory named `legacy_app_code` relative to the Dockerfile.

# Dockerfile.legacy
FROM php:7.4-fpm

WORKDIR /var/www/html

COPY legacy_app_code/ /var/www/html/

# Install necessary extensions (example)
RUN docker-php-ext-install pdo pdo_mysql mbstring

# Copy custom php.ini if needed
# COPY php.ini /usr/local/etc/php/conf.d/custom.ini

# Ensure permissions are correct for FPM
RUN chown -R www-data:www-data /var/www/html

EXPOSE 9000

Dockerfile for New PHP 9 Microservice (e.g., Users Service)

This Dockerfile assumes your new PHP 9 microservice code is in a directory named `users_service_code` relative to the Dockerfile.

# Dockerfile.users
FROM php:9.0-fpm

WORKDIR /var/www/users_service

COPY users_service_code/ /var/www/users_service/

# Install necessary extensions for PHP 9 (example)
RUN pecl install redis && docker-php-ext-enable redis
RUN docker-php-ext-install pdo pdo_mysql mbstring

# Composer dependencies
COPY users_service_code/composer.json users_service_code/composer.lock /var/www/users_service/
RUN composer install --no-dev --optimize-autoloader

# Copy application files
COPY users_service_code/ /var/www/users_service/

# Ensure permissions are correct for FPM
RUN chown -R www-data:www-data /var/www/users_service

EXPOSE 9000

Orchestrating with Docker Compose for Staged Rollout

Docker Compose allows us to define and run multi-container Docker applications. We can use it to manage the Nginx facade, the legacy PHP 7.x app, and the new PHP 9 services. This setup is invaluable for testing the routing and ensuring compatibility before a full production rollout.

# docker-compose.yml
version: '3.8'

services:
  nginx:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./legacy_app_code/public:/var/www/legacy_app/public # For static assets if any
    depends_on:
      - legacy_php
      - users_php

  legacy_php:
    build:
      context: .
      dockerfile: Dockerfile.legacy
    container_name: legacy_php_app
    expose:
      - "9000"
    volumes:
      - ./legacy_app_code:/var/www/html

  users_php:
    build:
      context: .
      dockerfile: Dockerfile.users
    container_name: users_php_service
    expose:
      - "9000"
    volumes:
      - ./users_service_code:/var/www/users_service

networks:
  default:
    driver: bridge

Updated Nginx Configuration for Docker Compose

This Nginx configuration will be mounted into the Nginx container. Note the use of service names (`legacy_php`, `users_php`) as hostnames, which Docker Compose resolves to the correct container IPs.

# nginx.conf (to be mounted into nginx container)

# Default server block for handling requests
server {
    listen 80 default_server;
    server_name _; # Catch-all

    location / {
        # Initially, proxy all to legacy app
        proxy_pass http://legacy_php;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Specific route for the new users service
    location /api/v1/users {
        proxy_pass http://users_php; # Route to the PHP 9 users service
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # If you have static assets in legacy app, handle them directly or via proxy
    # location ~* \.(css|js|jpg|jpeg|png|gif|ico)$ {
    #     root /var/www/legacy_app/public;
    #     expires 1d;
    # }

    # PHP-FPM configuration for legacy app (if Nginx needs to talk to it directly for non-API routes)
    # This part might be complex if legacy app uses index.php for everything.
    # For API-first migration, focus on API routes.
    location ~ \.php$ {
        # This block might be needed if legacy app serves non-API requests
        # and Nginx is configured to proxy to legacy_php:9000
        # However, in this example, we are proxying the entire request to legacy_php service.
        # If legacy_php is running PHP-FPM, Nginx would typically proxy to its FPM port.
        # For simplicity in this facade pattern, we proxy the HTTP request.
        # If legacy_php container itself runs PHP-FPM and Nginx needs to talk to it:
        # include snippets/fastcgi-php.conf;
        # fastcgi_pass legacy_php:9000; # Assuming legacy_php container exposes 9000
    }
}

Implementing the New PHP 9 Service

The new PHP 9 service should be a self-contained unit. For the `users` service, it might interact with a separate database or a shared database (with careful schema management). We’ll use a modern PHP framework or a microframework for this service.

Example: Simple Users Controller in PHP 9

<?php
// users_service_code/public/index.php (or equivalent entry point)

require __DIR__ . '/../vendor/autoload.php';

// Basic routing (using a simple router or a framework like Slim/Lumen)
$request_uri = $_SERVER['REQUEST_URI'];
$method = $_SERVER['REQUEST_METHOD'];

// Example: GET /api/v1/users
if ($method === 'GET' && $request_uri === '/api/v1/users') {
    header('Content-Type: application/json');
    echo json_encode(['users' => [
        ['id' => 1, 'name' => 'Alice (PHP 9)'],
        ['id' => 2, 'name' => 'Bob (PHP 9)']
    ]]);
    exit;
}

// Example: GET /api/v1/users/{id}
if (preg_match('/^\/api\/v1\/users\/(\d+)$/', $request_uri, $matches)) {
    $userId = $matches[1];
    header('Content-Type: application/json');
    // In a real app, fetch from DB
    echo json_encode(['id' => $userId, 'name' => 'User ' . $userId . ' (PHP 9)']);
    exit;
}

// Handle 404
http_response_code(404);
echo json_encode(['error' => 'Not Found']);
exit;
?>

Gradual Traffic Shifting and Zero Downtime

The Strangler Fig pattern’s power lies in its gradual nature. We can start by routing a small percentage of traffic to the new PHP 9 service. This is often achieved through advanced Nginx configurations or by using a more sophisticated API Gateway.

Blue/Green Deployment or Canary Releases with Nginx

For more granular control, we can implement canary releases. This involves deploying the new version alongside the old and routing a small subset of users to the new version. Nginx can be configured to route based on headers, cookies, or even a percentage of requests.

# Example: Routing 10% of traffic to users_php_v2 (a newer version)
# This requires a more advanced setup, potentially with multiple upstream blocks
# and conditional logic. For simplicity, let's illustrate a header-based switch.

# Define upstreams
upstream legacy_app_upstream {
    server legacy_php:9000; # Assuming legacy_php container exposes FPM on 9000
}

upstream users_service_v1 {
    server users_php:9000; # PHP 9 users service
}

# If you have a v2 of users service
# upstream users_service_v2 {
#     server users_php_v2:9000;
# }

server {
    listen 80;
    server_name yourdomain.com;

    location /api/v1/users {
        # Canary release logic: If 'x-canary-users' header is present, route to v2
        # if ($http_x_canary_users = "v2") {
        #     proxy_pass http://users_service_v2;
        # } else {
        #     proxy_pass http://users_service_v1;
        # }

        # Simple example: Route all /api/v1/users to v1 for now
        proxy_pass http://users_service_v1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Fallback for all other requests to legacy app
    location / {
        proxy_pass http://legacy_app_upstream; # Proxy to legacy PHP-FPM
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # If legacy app uses index.php and Nginx needs to talk to its FPM
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass legacy_app_upstream;
    }
}

Database Migration Strategies

Database changes are often the most challenging part of a migration. For the Strangler Fig pattern, you have a few options:

  • Shared Database, Separate Schemas: The new PHP 9 service uses its own schema within the existing database. This requires careful management of database credentials and permissions.
  • Data Synchronization: Implement a mechanism to synchronize data between the legacy database and the new database used by the PHP 9 service. This can be done via triggers, batch jobs, or change data capture (CDC) tools.
  • Database Per Service: The ideal microservices approach, where each service owns its data. This is the most complex to achieve during a gradual migration and might involve a period of data duplication and synchronization.

During the transition, the legacy application might need to read from the new database, or the new service might need to read from the legacy database. This necessitates careful API design and data access layers.

Production Deployment with Kubernetes

For production, Docker Compose is insufficient. Kubernetes (K8s) is the de facto standard for orchestrating containerized applications. We’ll deploy our Nginx facade, legacy PHP 7.x app (potentially in a read-only mode or scaled down), and new PHP 9 microservices as K8s Deployments and Services.

Kubernetes Deployment Example (Conceptual)

This is a simplified representation. Actual K8s manifests would be more detailed, including Ingress controllers, Persistent Volumes, ConfigMaps, etc.

# deployment-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-facade
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-facade
  template:
    metadata:
      labels:
        app: nginx-facade
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
        volumeMounts:
        - name: nginx-config-volume
          mountPath: /etc/nginx/conf.d
      volumes:
      - name: nginx-config-volume
        configMap:
          name: nginx-config # A K8s ConfigMap containing your nginx.conf

---
# service-nginx.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-facade-service
spec:
  selector:
    app: nginx-facade
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: LoadBalancer # Or ClusterIP if using an Ingress Controller

---
# deployment-users-php.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: users-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: users-service
  template:
    metadata:
      labels:
        app: users-service
    spec:
      containers:
      - name: users-php
        image: your-docker-registry/users-service:php9-v1.0.0 # Your PHP 9 image
        ports:
        - containerPort: 9000 # FPM port
        # ... env vars for DB connection, etc.

---
# service-users-php.yaml
apiVersion: v1
kind: Service
metadata:
  name: users-service-internal
spec:
  selector:
    app: users-service
  ports:
  - protocol: TCP
    port: 9000
    targetPort: 9000
  type: ClusterIP # Internal service, accessed by Nginx

The Nginx Ingress controller in Kubernetes would then be configured to route traffic based on paths to the `nginx-facade-service` and `users-service-internal`. The Nginx configuration within the `nginx-facade` deployment would be updated to point to the K8s Service names (`users-service-internal`).

Monitoring and Rollback

Throughout the migration, robust monitoring is essential. Track error rates, latency, and resource utilization for both the legacy and new services. Implement health checks for all containers. In case of issues, the Strangler Fig pattern allows for quick rollbacks by simply reverting the Nginx routing rules to point exclusively to the legacy system.

Conclusion

Migrating a legacy PHP 7.x application to PHP 9 with zero downtime is a marathon, not a sprint. The Strangler Fig pattern, combined with Docker orchestration (Compose for development/testing, Kubernetes for production), provides a structured and safe approach. By incrementally replacing functionality and carefully managing traffic, you can achieve a smooth transition to a modern, performant PHP 9 environment without impacting users.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Migrating Legacy PHP 7.x Applications to PHP 9 with Zero Downtime: A Deep Dive into Strangler Fig Pattern and Docker Orchestration
  • Orchestrating Microservices with PHP 8/9 and Laravel: A Deep Dive into Docker Swarm and AWS ECS
  • Leveraging PHP 8 JIT and Swoole for High-Performance, Event-Driven Laravel Applications on AWS Lambda
  • Leveraging AWS Lambda and API Gateway for Scalable, Serverless WordPress REST APIs
  • Beyond the Monolith: Architecting Scalable, Event-Driven WordPress Headless with PHP 8.3 and AWS Lambda

Categories

  • apache (1)
  • AWS (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (664)
  • Desktop Applications (14)
  • DevOps (72)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (75)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • Performance & Security Optimization (8)
  • PHP (260)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (10)
  • Python (20)
  • Ruby on Rails (1)
  • Security (1)
  • Security & Compliance (650)
  • SEO & Growth (492)
  • Server (118)
  • Softwares (1)
  • Ubuntu (9)
  • Uncategorized (515)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (136)
  • WordPress Plugin Development (728)
  • WordPress Theme Development (357)

Recent Posts

  • Migrating Legacy PHP 7.x Applications to PHP 9 with Zero Downtime: A Deep Dive into Strangler Fig Pattern and Docker Orchestration
  • Orchestrating Microservices with PHP 8/9 and Laravel: A Deep Dive into Docker Swarm and AWS ECS
  • Leveraging PHP 8 JIT and Swoole for High-Performance, Event-Driven Laravel Applications on AWS Lambda

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (728)
  • Debugging & Troubleshooting (664)
  • Security & Compliance (650)
  • Uncategorized (515)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala