Dockerizing and Orchestrating Legacy Laravel Systems on Modern Google Cloud Infrastructure
Containerizing the Laravel Application
The first step in modernizing a legacy Laravel application for cloud deployment is to containerize it. This involves creating a Dockerfile that encapsulates the application’s environment, dependencies, and runtime. For a typical Laravel application, this means including PHP, Composer, and any necessary PHP extensions, along with the application code itself.
We’ll start with a multi-stage build to keep our final image lean. The first stage will handle dependency installation, and the second will copy only the necessary artifacts to a minimal base image.
Dockerfile for Laravel
# Stage 1: Build dependencies
FROM composer:latest as builder
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction
COPY . .
RUN composer dump-autoload --optimize --no-dev
# Stage 2: Production image
FROM php:8.2-fpm-alpine
# Install necessary PHP extensions
RUN apk add --no-cache \
acl \
file \
fpm \
git \
icu-dev \
libzip-dev \
libpng-dev \
libjpeg-turbo-dev \
oniguruma-dev \
postgresql-dev \
supervisor \
zip \
&& docker-php-ext-configure gd --with-jpeg --with-freetype \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install -j$(nproc) opcache \
&& docker-php-ext-install -j$(nproc) pdo pdo_pgsql zip
# Copy application code and dependencies from builder stage
COPY --from=builder /app /app
# Set working directory
WORKDIR /app
# Copy the application's .env.example to .env if it doesn't exist
RUN cp .env.example .env
# Install Node.js and npm for asset compilation (if needed)
# This can be a separate stage if you prefer to keep the final image smaller
RUN apk add --no-cache nodejs npm
RUN npm install
RUN npm run build
# Clean up npm cache
RUN npm cache clean --force
# Configure Supervisor for running background processes (e.g., queue workers)
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Expose port 9000 for PHP-FPM
EXPOSE 9000
# Start Supervisor to manage PHP-FPM and other processes
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]
Supervisord Configuration
To manage PHP-FPM and potential background workers (like queue workers), we’ll use Supervisor. This ensures that critical processes are automatically restarted if they fail.
[supervisord] nodaemon=true user=root [program:php-fpm] command=/usr/local/sbin/php-fpm --nodaemonize --fpm-config /usr/local/etc/php-fpm.conf autostart=true autorestart=true priority=10 stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:queue-worker] command=php artisan queue:work --tries=3 --timeout=60 process_name=%(program_name)s_%(process_num)02d numprocs=2 autostart=true autorestart=true priority=20 stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 user=www-data directory=/app
Database and Cache Services on Google Cloud
Legacy applications often rely on traditional database setups. For modernization on Google Cloud, we’ll leverage managed services like Cloud SQL for PostgreSQL and Memorystore for Redis. This offloads operational burden and provides scalability and reliability.
Cloud SQL for PostgreSQL Configuration
When deploying to Google Cloud, your Laravel application will need to connect to a Cloud SQL instance. The most secure and recommended method is to use the Cloud SQL Auth Proxy. This proxy handles secure, encrypted connections to your database without requiring you to manage SSL certificates directly within your application’s Docker container.
First, ensure you have the Cloud SQL Auth Proxy binary available. You can download it or, more practically for containerization, include it in your Docker image. For simplicity in this example, we’ll assume it’s available in the environment where the container runs, or you can add it to your Dockerfile.
Your Laravel application’s database configuration (`config/database.php`) will need to be updated to point to the proxy. The proxy typically runs on `127.0.0.1:5432` (or a different port if configured) and connects to your Cloud SQL instance via its instance connection name.
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'), // This will be the proxy's host
'port' => env('DB_PORT', '5432'), // This will be the proxy's port
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'schema' => 'public',
'sslmode' => 'prefer',
],
In your `.env` file, you’ll set the connection details to point to the proxy:
DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=your_database_name DB_USERNAME=your_db_user DB_PASSWORD=your_db_password
Memorystore for Redis Configuration
For caching and session management, Redis is a common choice. Google Cloud’s Memorystore provides a managed Redis service. Your Laravel application will connect to the Memorystore instance’s IP address.
Update your `.env` file with the Memorystore instance details:
REDIS_HOST=your-memorystore-instance-ip REDIS_PASSWORD=null REDIS_PORT=6379
And in `config/cache.php` and `config/session.php`, ensure you’re using the Redis driver and that the configuration correctly picks up these environment variables.
'default' => env('CACHE_DRIVER', 'file'),
'stores' => [
// ... other stores
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
],
// ...
],
'redis' => [
'client' => 'phpredis', // Or 'predis' if you prefer
'default' => [
'host' => env('REDIS_HOST'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT'),
'database' => 0,
],
],
Orchestration with Google Kubernetes Engine (GKE)
To deploy and manage your containerized Laravel application at scale, Google Kubernetes Engine (GKE) is the ideal choice. It provides a robust platform for orchestration, scaling, and self-healing.
Kubernetes Deployment Manifests
We’ll define Kubernetes resources using YAML manifests. This includes Deployments for managing application pods, Services for exposing the application, and potentially Ingress for external access.
First, a Deployment to manage your Laravel application pods. This will specify the Docker image to use, the number of replicas, and how to perform rolling updates.
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-app
labels:
app: laravel
spec:
replicas: 3 # Adjust based on your needs
selector:
matchLabels:
app: laravel
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
app: laravel
spec:
containers:
- name: laravel-app
image: gcr.io/your-gcp-project-id/your-laravel-image:latest # Replace with your image
ports:
- containerPort: 80 # Or the port your web server inside the container listens on
env:
- name: APP_ENV
value: "production"
- name: APP_DEBUG
value: "false"
- name: APP_URL
value: "https://your-domain.com" # Set your application URL
- name: DB_CONNECTION
value: "pgsql"
- name: DB_HOST
value: "127.0.0.1" # Points to the Cloud SQL Auth Proxy
- name: DB_PORT
value: "5432"
- name: DB_DATABASE
valueFrom:
secretKeyRef:
name: db-secrets
key: db-name
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-secrets
key: db-user
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secrets
key: db-password
- name: REDIS_HOST
value: "your-memorystore-instance-ip" # Direct IP for Memorystore
- name: REDIS_PORT
value: "6379"
# Add other necessary environment variables
livenessProbe:
httpGet:
path: /healthz # Create a health check endpoint in your Laravel app
port: 80
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
# If using Cloud SQL Auth Proxy as a sidecar
# - name: cloud-sql-proxy
# image: gcr.io/cloudsql-docker/gce-proxy:1.33.0 # Use the latest version
# command:
# - "/cloud_sql_proxy"
# - "-instances=your-gcp-project-id:your-region:your-cloudsql-instance-name=tcp:5432"
# resources:
# requests:
# cpu: "50m"
# memory: "64Mi"
# limits:
# cpu: "100m"
# memory: "128Mi"
# securityContext:
# runAsNonRoot: true
Next, a Service to expose your application within the cluster. This provides a stable IP address and DNS name for your pods.
apiVersion: v1
kind: Service
metadata:
name: laravel-app-service
spec:
selector:
app: laravel
ports:
- protocol: TCP
port: 80
targetPort: 80 # The port your application container listens on
type: ClusterIP # Or LoadBalancer if you want a direct external IP
For external access, an Ingress resource is typically used. This routes external HTTP(S) traffic to your Service. You’ll need to have an Ingress controller (like GKE’s built-in Load Balancer or Nginx Ingress Controller) installed.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: laravel-ingress
annotations:
# For GKE Load Balancer:
kubernetes.io/ingress.class: "gce"
# For Nginx Ingress Controller:
# nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: your-domain.com # Replace with your domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: laravel-app-service
port:
number: 80
Secrets Management
Sensitive information like database credentials should not be hardcoded in your manifests. Kubernetes Secrets are the standard way to manage these. You can create secrets manually or use tools like Google Secret Manager integrated with GKE.
kubectl create secret generic db-secrets \ --from-literal=db-name='your_database_name' \ --from-literal=db-user='your_db_user' \ --from-literal=db-password='your_db_password'
CI/CD Pipeline for Automated Deployments
A robust CI/CD pipeline is crucial for modernizing legacy systems. We’ll outline a workflow using Cloud Build, which integrates seamlessly with Google Cloud services.
Cloud Build Configuration
Create a `cloudbuild.yaml` file in your project’s root directory. This file defines the build steps.
steps: # 1. Build the Docker image - name: 'gcr.io/cloud-builders/docker' args: ['build', '-t', 'gcr.io/$PROJECT_ID/your-laravel-image:$COMMIT_SHA', '.'] # 2. Push the Docker image to Google Container Registry - name: 'gcr.io/cloud-builders/docker' args: ['push', 'gcr.io/$PROJECT_ID/your-laravel-image:$COMMIT_SHA'] # 3. 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-gke-zone' # e.g., us-central1-a - 'CLOUDSDK_CONTAINER_CLUSTER=your-gke-cluster-name' # Your GKE cluster name - 'PROJECT_ID=$PROJECT_ID' # Pass project ID to the build step # 4. (Optional) Update the deployment with the new image tag # This step is useful if your deployment.yaml uses a fixed tag like 'latest' # and you want to ensure it picks up the specific commit SHA. # Alternatively, you can use kustomize or Helm for more advanced deployments. - name: 'gcr.io/cloud-builders/kubectl' args: - 'set' - 'image' - 'deployment/laravel-app' # Name of your deployment - 'laravel-app=gcr.io/$PROJECT_ID/your-laravel-image:$COMMIT_SHA' env: - 'CLOUDSDK_COMPUTE_ZONE=your-gke-zone' - 'CLOUDSDK_CONTAINER_CLUSTER=your-gke-cluster-name' images: - 'gcr.io/$PROJECT_ID/your-laravel-image:$COMMIT_SHA' options: logging: CLOUD_LOGGING_ONLY
To trigger this build, you can set up a webhook in Cloud Build to listen for pushes to your Git repository (e.g., Cloud Source Repositories, GitHub, Bitbucket). When a push occurs, Cloud Build will execute the steps defined in `cloudbuild.yaml`, building your Docker image, pushing it to GCR, and deploying the new version to your GKE cluster.
Monitoring and Logging
Effective monitoring and logging are critical for maintaining production systems. Google Cloud offers integrated solutions for this.
Cloud Logging and Cloud Monitoring
By default, containers running on GKE will have their stdout and stderr streams sent to Cloud Logging. You can access these logs via the Google Cloud Console or the `gcloud` CLI.
gcloud logging read "resource.type=k8s_container AND resource.labels.cluster_name=your-gke-cluster-name AND resource.labels.namespace_name=default AND resource.labels.pod_name=laravel-app-*" --limit=100
For more advanced logging, consider configuring your application to write logs to files within the container and then using a logging agent (like Fluentd or Fluent Bit) deployed as a DaemonSet in GKE to collect and forward these logs to Cloud Logging with structured metadata. This is especially useful for application-specific logs.
Cloud Monitoring can be used to set up dashboards and alerts based on metrics from your GKE cluster, pods, and managed services like Cloud SQL and Memorystore. You can monitor CPU usage, memory, network traffic, database connections, and more. Set up alerts for critical thresholds to proactively address potential issues.