Beyond `docker-compose`: Orchestrating Microservices with Kubernetes and PHP-FPM for Extreme Laravel Scalability
Kubernetes as the Microservice Orchestration Layer
While docker-compose is excellent for local development and simple deployments, it quickly becomes a bottleneck when scaling microservices in production. Kubernetes (K8s) offers a robust, declarative, and highly available platform for managing containerized applications. For a Laravel-based microservice architecture, K8s provides the necessary primitives to manage deployments, scaling, networking, and resilience.
We’ll focus on deploying a typical Laravel application, which often relies on PHP-FPM for request processing and a separate web server (like Nginx or Apache) for serving static assets and proxying requests. In a K8s environment, this translates to distinct container images and Kubernetes resources.
Containerizing Laravel with PHP-FPM
The core of our Laravel application will be containerized using PHP-FPM. This separates the application logic from the web server, allowing independent scaling and management. We’ll build a Docker image that includes our Laravel code, dependencies, and the PHP-FPM process manager.
Consider a typical Dockerfile for a PHP-FPM service:
# Use an official PHP image as a parent image
FROM php:8.2-fpm
# Set the working directory in the container
WORKDIR /var/www/html
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
zip \
unzip \
libzip-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libonig-dev \
libxml2-dev \
libssl-dev \
libcurl4-openssl-dev \
libsqlite3-dev \
libpq-dev \
acl \
vim \
cron \
&& rm -rf /var/lib/apt/lists/*
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install pdo pdo_mysql zip exif pcntl opcache sockets \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& docker-php-ext-install bcmath \
&& docker-php-ext-install sockets \
&& docker-php-ext-install opcache
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
# Copy the application code
COPY . .
# Install Composer dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Set permissions for storage and bootstrap/cache
RUN chown -R www-data:www-data storage bootstrap/cache && chmod -R 775 storage bootstrap/cache
# Expose port 9000 for PHP-FPM
EXPOSE 9000
# Start PHP-FPM
CMD ["php-fpm"]
Key points:
- We use a specific PHP version (
8.2-fpm) to ensure consistency. - Essential system packages and PHP extensions are installed.
- Composer is installed globally for dependency management.
- Application code is copied, and dependencies are installed with optimization flags for production.
- Permissions for
storageandbootstrap/cacheare set correctly for thewww-datauser. - The container exposes port 9000, the default for PHP-FPM.
- The
CMDis set tophp-fpm, ensuring the FPM process runs when the container starts.
Kubernetes Deployment for PHP-FPM
In Kubernetes, we define our application’s desired state using YAML manifests. A Deployment resource manages stateless applications, ensuring a specified number of replicas are running and handling updates. For our PHP-FPM service, this would look something like:
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-php-fpm
labels:
app: laravel
tier: php-fpm
spec:
replicas: 3 # Start with 3 replicas
selector:
matchLabels:
app: laravel
tier: php-fpm
template:
metadata:
labels:
app: laravel
tier: php-fpm
spec:
containers:
- name: php-fpm
image: your-docker-registry/laravel-app-php-fpm:latest # Replace with your image
ports:
- containerPort: 9000
livenessProbe:
exec:
command: ["kill", "-0", "1"] # Basic check if the process is alive
initialDelaySeconds: 10
periodSeconds: 5
readinessProbe:
exec:
command: ["sh", "-c", "php-fpm -t"] # Check FPM configuration
initialDelaySeconds: 10
periodSeconds: 5
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
volumeMounts:
- name: app-storage
mountPath: /var/www/html/storage
- name: app-bootstrap-cache
mountPath: /var/www/html/bootstrap/cache
volumes:
- name: app-storage
emptyDir: {} # Placeholder, will be replaced by persistent storage
- name: app-bootstrap-cache
emptyDir: {} # Placeholder, will be replaced by persistent storage
Explanation:
replicas: 3: We specify an initial desired state of 3 running PHP-FPM pods. K8s will ensure this count is maintained.selector: This links the Deployment to the Pods it manages.template: Defines the Pod specification.image: Points to your container image in a registry.ports: Exposes the PHP-FPM port.livenessProbeandreadinessProbe: Crucial for K8s to manage pod health. The liveness probe restarts a failing container, while the readiness probe determines if a pod is ready to receive traffic. Thephp-fpm -tcommand is a simple way to check FPM configuration validity.resources: Defines CPU and memory requests and limits. This is vital for resource management and preventing noisy neighbor issues.volumeMountsandvolumes: For stateful data like logs, sessions, or cached views, we’d typically use Persistent Volumes (PVs) and Persistent Volume Claims (PVCs). For simplicity here,emptyDiris used as a placeholder, but in production, you’d configure appropriate storage solutions (e.g., NFS, Ceph, cloud provider storage).
Kubernetes Service for PHP-FPM
A Kubernetes Service provides a stable IP address and DNS name for a set of Pods. This allows other services (like our web server) to discover and communicate with the PHP-FPM pods without needing to know their individual IP addresses, which can change.
apiVersion: v1
kind: Service
metadata:
name: laravel-php-fpm-service
labels:
app: laravel
spec:
selector:
app: laravel
tier: php-fpm # Selects pods with these labels
ports:
- protocol: TCP
port: 9000
targetPort: 9000 # The port PHP-FPM is listening on
type: ClusterIP # Only accessible within the cluster
This ClusterIP service makes the PHP-FPM pods accessible at the stable DNS name laravel-php-fpm-service.your-namespace.svc.cluster.local (or just laravel-php-fpm-service within the same namespace) on port 9000.
Containerizing the Web Server (Nginx)
A separate container will handle incoming HTTP requests, serve static assets, and proxy dynamic requests to the PHP-FPM service. Nginx is a common choice.
# Use an official Nginx image FROM nginx:alpine # Remove default Nginx configuration RUN rm /etc/nginx/conf.d/default.conf # Copy custom Nginx configuration COPY nginx.conf /etc/nginx/conf.d/default.conf # Copy static assets (if any, or mount them) # COPY public/ /usr/share/nginx/html/ # Expose port 80 EXPOSE 80 # Start Nginx CMD ["nginx", "-g", "daemon off;"]
And the corresponding nginx.conf:
server {
listen 80;
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/html; # Assuming your Laravel public dir is mapped here
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
# Use the Kubernetes service name for PHP-FPM
fastcgi_pass laravel-php-fpm-service:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
# Serve static files directly
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp)$ {
expires 1y;
add_header Cache-Control "public";
}
}
Crucially, the fastcgi_pass directive points to the Kubernetes Service name for PHP-FPM (laravel-php-fpm-service:9000). This is how Nginx discovers and communicates with the PHP-FPM pods.
Kubernetes Deployment and Service for Nginx
Similar to PHP-FPM, we define a Deployment for Nginx and a Service to expose it externally.
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-nginx
labels:
app: laravel
tier: web
spec:
replicas: 2 # Fewer replicas for the web tier might suffice
selector:
matchLabels:
app: laravel
tier: web
template:
metadata:
labels:
app: laravel
tier: web
spec:
containers:
- name: nginx
image: your-docker-registry/laravel-app-nginx:latest # Replace with your image
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: / # Basic check on Nginx root
port: 80
initialDelaySeconds: 10
periodSeconds: 5
readinessProbe:
httpGet:
path: / # Basic check on Nginx root
port: 80
initialDelaySeconds: 10
periodSeconds: 5
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "256Mi"
volumeMounts:
- name: app-storage-nginx # Mount storage for logs, etc.
mountPath: /var/log/nginx
- name: app-public-assets # Mount public assets if not baked into image
mountPath: /var/www/html # Or wherever Nginx expects them
volumes:
- name: app-storage-nginx
emptyDir: {} # Use Persistent Volume for logs in production
- name: app-public-assets
emptyDir: {} # Use Persistent Volume or ConfigMap for static assets
apiVersion: v1
kind: Service
metadata:
name: laravel-nginx-service
labels:
app: laravel
spec:
selector:
app: laravel
tier: web
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancer # Expose externally via cloud provider LB or NodePort
The type: LoadBalancer for the Nginx service is key for external access. In cloud environments (AWS, GCP, Azure), this automatically provisions a cloud load balancer. For on-premises or bare-metal setups, you might use NodePort and manage external access via an external load balancer (like HAProxy or Traefik) or use an Ingress Controller.
Managing Persistent Data and Configuration
Stateful components require careful handling in Kubernetes:
- Database: For production, never run your database inside a standard K8s Deployment. Use managed database services (RDS, Cloud SQL) or deploy a dedicated database cluster using operators (e.g., Crunchy Data PostgreSQL Operator, Percona Operator for MySQL).
- Cache/Sessions: Redis or Memcached should be deployed as separate StatefulSets or managed services.
- File Storage (e.g., Laravel
storagedirectory): Use Persistent Volumes (PVs) and Persistent Volume Claims (PVCs). This ensures data persists across pod restarts and can be shared if necessary. For shared access (e.g., multiple Nginx pods needing to write logs), consider NFS or distributed file systems. - Configuration: Sensitive information like database credentials, API keys, etc., should be managed using Kubernetes
Secrets. Non-sensitive configuration can be managed viaConfigMaps. These can be mounted as files into pods or injected as environment variables.
Example of mounting a ConfigMap for Nginx configuration:
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
data:
nginx.conf: |
# ... your nginx.conf content here ...
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-nginx
spec:
# ... other spec ...
template:
# ... other template spec ...
spec:
containers:
- name: nginx
# ... other container spec ...
volumeMounts:
- name: nginx-config-volume
mountPath: /etc/nginx/conf.d
volumes:
- name: nginx-config-volume
configMap:
name: nginx-config
Scaling and Observability
Kubernetes excels at scaling. You can manually adjust replica counts or use the HorizontalPodAutoscaler (HPA) to automatically scale based on CPU or memory utilization.
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: laravel-php-fpm-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: laravel-php-fpm
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # Scale up when CPU utilization reaches 70%
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70 # Scale up when Memory utilization reaches 70%
For observability, integrate:
- Logging: Deploy a cluster-level logging agent (e.g., Fluentd, Filebeat) to collect logs from all pods and forward them to a centralized logging system (Elasticsearch, Loki).
- Metrics: Use Prometheus to scrape metrics from your applications and Kubernetes components. Grafana can then be used for visualization.
- Tracing: Implement distributed tracing (e.g., Jaeger, Zipkin) to track requests across microservices.
Conclusion: Moving Beyond Compose
Transitioning from docker-compose to Kubernetes for a Laravel microservice architecture unlocks true scalability, resilience, and manageability. By defining your PHP-FPM and Nginx services as distinct Deployments and Services, leveraging Kubernetes’ health checks, and carefully managing persistent data and configuration, you can build robust, high-performance applications capable of handling significant load. This approach provides the foundation for advanced patterns like service meshes, CI/CD integration, and sophisticated auto-scaling strategies.