Dockerizing and Orchestrating Legacy PHP Systems on Modern Google Cloud Infrastructure
Containerizing the Legacy PHP Application
The first critical step is to encapsulate the existing PHP application within a Docker container. This involves creating a Dockerfile that defines the environment, dependencies, and startup commands for your application. For a typical legacy PHP application, this often means dealing with older PHP versions, specific extensions, and potentially a monolithic codebase.
Let’s assume a common scenario: a PHP application running on Apache with MySQL. We’ll aim for a multi-stage build to keep the final image lean.
Dockerfile for PHP Application
# Stage 1: Build dependencies (if any complex compilation is needed)
# This stage is often omitted for simpler PHP apps but useful for extensions
# FROM php:8.1-fpm as builder
# RUN docker-php-ext-install pdo_mysql && docker-php-ext-enable pdo_mysql
# Stage 2: Production image
FROM php:8.1-apache
# Install necessary PHP extensions. Adjust as per your application's needs.
# Common ones include: mysqli, pdo_mysql, gd, intl, zip, mbstring
RUN apt-get update && apt-get install -y \
libzip-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libicu-dev \
unzip \
git \
vim \
cron \
&& rm -rf /var/lib/apt/lists/* \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd pdo_mysql zip intl mbstring opcache \
&& a2enmod rewrite
# Copy application code
COPY . /var/www/html/
# Set working directory
WORKDIR /var/www/html/
# Install Composer dependencies (if applicable)
# Ensure composer.json and composer.lock are in the root of your COPY source
COPY composer.json composer.lock /var/www/html/
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Clean up composer cache
RUN rm -rf /root/.composer/cache
# Configure Apache for PHP
COPY apache/000-default.conf /etc/apache2/sites-available/000-default.conf
# Expose port 80
EXPOSE 80
# Set permissions (adjust if your application needs specific user/group)
RUN chown -R www-data:www-data /var/www/html/ && chmod -R 755 /var/www/html/
# Enable Apache modules
RUN a2enmod headers expires ssl
# Enable opcache
RUN docker-php-ext-enable opcache
# Enable cron jobs if any
# COPY cron/crontab /etc/cron.d/my-cron
# RUN chmod 0644 /etc/cron.d/my-cron && crontab /etc/cron.d/my-cron && touch /var/log/cron.log
# Start Apache in foreground
CMD ["apache2-foreground"]
Apache Configuration Example
Create a directory named apache in the same directory as your Dockerfile and place a configuration file, e.g., 000-default.conf, inside it.
<VirtualHost *:80>
DocumentRoot /var/www/html/
<Directory /var/www/html/>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
After creating the Dockerfile and any necessary supporting files (like the Apache config), build the Docker image:
docker build -t your-dockerhub-username/legacy-php-app:v1.0 .
Push this image to a container registry that Google Cloud can access, such as Google Container Registry (GCR) or Artifact Registry.
docker push your-dockerhub-username/legacy-php-app:v1.0
Orchestrating with Google Kubernetes Engine (GKE)
Google Kubernetes Engine (GKE) is the ideal platform for orchestrating containerized legacy applications. It provides robust features for deployment, scaling, and management.
Kubernetes Deployment Manifest
We’ll define a Kubernetes Deployment to manage our PHP application pods and a Service to expose it.
apiVersion: apps/v1
kind: Deployment
metadata:
name: legacy-php-app-deployment
labels:
app: legacy-php-app
spec:
replicas: 3 # Start with 3 replicas for HA
selector:
matchLabels:
app: legacy-php-app
template:
metadata:
labels:
app: legacy-php-app
spec:
containers:
- name: legacy-php-app
image: gcr.io/your-gcp-project-id/legacy-php-app:v1.0 # Replace with your GCR/Artifact Registry path
ports:
- containerPort: 80
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /healthz.php # Assuming you have a health check endpoint
port: 80
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /healthz.php
port: 80
initialDelaySeconds: 5
periodSeconds: 10
env: # Example environment variables
- name: DB_HOST
value: "mysql-service" # This will be the Kubernetes Service name for your MySQL DB
- name: DB_USER
valueFrom:
secretKeyRef:
name: mysql-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-credentials
key: password
- name: DB_NAME
value: "legacy_db"
# If your app needs persistent storage for uploads/logs, configure volumes here
# volumes:
# - name: app-storage
# persistentVolumeClaim:
# claimName: legacy-php-app-pvc
# containers:
# - name: legacy-php-app
# ...
# volumeMounts:
# - name: app-storage
# mountPath: /var/www/html/uploads # Example mount path
---
apiVersion: v1
kind: Service
metadata:
name: legacy-php-app-service
spec:
selector:
app: legacy-php-app
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP # Use LoadBalancer for external access, or Ingress
Database Considerations
For the database, you have several options:
- Managed Cloud SQL Instance: This is the recommended approach for production. Provision a Cloud SQL instance (MySQL, PostgreSQL, etc.) and configure your GKE application to connect to it. You’ll need to manage network connectivity (e.g., using Private IP or Cloud SQL Auth Proxy).
- Database within Kubernetes: Deploy MySQL or another database directly within your GKE cluster using a StatefulSet. This is simpler for development/testing but requires more operational overhead for production (backups, HA, patching).
If using Cloud SQL, you’ll typically use the Cloud SQL Auth Proxy sidecar container or configure Private IP for direct access. For secrets like database credentials, use Kubernetes Secrets, ideally populated from Google Secret Manager.
Ingress for External Access
To expose your application to the internet, you’ll use an Ingress controller. Google Cloud’s Managed Instance Group (MIG) based Ingress is a common choice.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: legacy-php-app-ingress
annotations:
kubernetes.io/ingress.class: "gce" # For Google Cloud's native Ingress
# If using cert-manager for SSL:
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: legacy-php-app-service
port:
number: 80
# If using TLS/SSL:
# tls:
# - hosts:
# - your-domain.com
# secretName: legacy-php-app-tls-secret # Kubernetes secret containing your TLS cert and key
Apply these manifests to your GKE cluster:
kubectl apply -f deployment.yaml kubectl apply -f service.yaml kubectl apply -f ingress.yaml
Monitoring and Logging
Robust monitoring and logging are crucial for any production system, especially when migrating legacy applications. GKE integrates well with Google Cloud’s operations suite (formerly Stackdriver).
Enabling Cloud Operations for GKE
Ensure that the Cloud Operations for GKE add-on is enabled for your GKE cluster. This automatically collects logs and metrics from your pods and nodes.
You can verify this during cluster creation or by updating an existing cluster:
gcloud container clusters update YOUR_CLUSTER_NAME \
--update-addons=CloudLogging=ENABLED,CloudMonitoring=ENABLED \
--zone=YOUR_CLUSTER_ZONE # or --region=YOUR_CLUSTER_REGION
Custom Metrics and Log Analysis
For application-specific metrics, consider using Prometheus and exporting them to Cloud Monitoring, or directly instrumenting your PHP application to send metrics to Cloud Monitoring’s custom metrics API. For logs, ensure your PHP application writes to stdout and stderr, which GKE will capture. You can then use Cloud Logging’s powerful query language to analyze these logs.
Example of writing logs to stdout/stderr in PHP:
<?php
error_log("This is an informational message.");
file_put_contents('php://stderr', "This is an error message.\n");
?>
CI/CD Pipeline Integration
Automating the build, test, and deployment process is key to efficient operations. Cloud Build is Google Cloud’s CI/CD platform that integrates seamlessly with GKE and GCR/Artifact Registry.
Cloud Build Configuration
Create a cloudbuild.yaml file in your repository’s root:
steps: # Build the Docker image - name: 'gcr.io/cloud-builders/docker' args: ['build', '-t', 'gcr.io/$PROJECT_ID/legacy-php-app:$COMMIT_SHA', '.'] id: 'Build Docker Image' # Push the Docker image to GCR/Artifact Registry - name: 'gcr.io/cloud-builders/docker' args: ['push', 'gcr.io/$PROJECT_ID/legacy-php-app:$COMMIT_SHA'] id: 'Push Docker Image' # Deploy to GKE - name: 'gcr.io/cloud-builders/kubectl' args: - 'apply' - '-f' - 'kubernetes/deployment.yaml' # Path to your deployment manifest env: - 'CLOUDSDK_COMPUTE_ZONE=YOUR_CLUSTER_ZONE' # Or CLOUDSDK_COMPUTE_REGION - 'CLOUDSDK_CONTAINER_CLUSTER=YOUR_CLUSTER_NAME' id: 'Deploy to GKE' waitFor: ['Push Docker Image'] # Ensure image is pushed before deploying # Optional: Update deployment with new image tag - name: 'gcr.io/cloud-builders/kubectl' args: - 'set' - 'image' - 'deployment/legacy-php-app-deployment' - 'legacy-php-app=gcr.io/$PROJECT_ID/legacy-php-app:$COMMIT_SHA' env: - 'CLOUDSDK_COMPUTE_ZONE=YOUR_CLUSTER_ZONE' # Or CLOUDSDK_COMPUTE_REGION - 'CLOUDSDK_CONTAINER_CLUSTER=YOUR_CLUSTER_NAME' id: 'Update Deployment Image' waitFor: ['Deploy to GKE'] images: - 'gcr.io/$PROJECT_ID/legacy-php-app:$COMMIT_SHA' options: logging: CLOUD_LOGGING_ONLY # If using private GKE clusters, you might need to configure network access for Cloud Build # machineType: 'N1_HIGHCPU_8' # Example for larger builds
Configure a trigger in Cloud Build to watch your repository (e.g., on push to the `main` branch) and execute this build configuration. Ensure the Cloud Build service account has the necessary permissions to push to GCR/Artifact Registry and deploy to GKE.