Orchestrating Microservices with Kubernetes: A Deep Dive into Scaling Laravel Applications with Docker
Containerizing Laravel: The Dockerfile Foundation
Before orchestrating, we must containerize. A robust Dockerfile is the bedrock of a scalable Laravel application on Kubernetes. This example targets a production-ready setup, minimizing image size and optimizing for security and performance.
We’ll leverage a multi-stage build. The first stage compiles assets and dependencies, while the second stage copies only the necessary artifacts to a lean runtime image. This significantly reduces the final image size, leading to faster deployments and reduced storage costs.
# Stage 1: Build dependencies and assets
FROM php:8.2-fpm-alpine AS builder
# Install system dependencies
RUN apk add --no-cache \
git \
zip \
unzip \
icu-dev \
libzip-dev \
libpng-dev \
jpeg-dev \
freetype-dev \
imagemagick-dev \
nodejs \
npm \
supervisor \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install -j$(nproc) intl \
&& docker-php-ext-install -j$(nproc) zip
# Set working directory
WORKDIR /var/www/html
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Copy application files
COPY . .
# Install Composer dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Install Node.js dependencies and build assets
RUN npm install && npm run build
# Clean up development dependencies and cache
RUN rm -rf node_modules vendor/ && composer install --no-dev --optimize-autoloader --no-interaction --no-cache
# Stage 2: Production runtime
FROM php:8.2-fpm-alpine
# Install system dependencies for runtime
RUN apk add --no-cache \
icu \
libzip \
libpng \
jpeg \
freetype \
imagemagick \
supervisor \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install -j$(nproc) intl \
&& docker-php-ext-install -j$(nproc) zip
# Copy compiled assets and dependencies from builder stage
COPY --from=builder /var/www/html/vendor ./vendor
COPY --from=builder /var/www/html/public/build ./public/build
COPY --from=builder /var/www/html/storage ./storage
COPY --from=builder /var/www/html/bootstrap/cache ./bootstrap/cache
# Copy application code (excluding dev files)
COPY . .
# Ensure correct permissions for storage and bootstrap/cache
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
# Copy supervisor configuration
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Expose port
EXPOSE 9000
# Start supervisor
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
The supervisord.conf file is crucial for managing PHP-FPM and potentially other background processes like queues. Here’s a sample:
[supervisord] nodaemon=true user=root [program:php-fpm] command=/usr/local/sbin/php-fpm -y /usr/local/etc/php-fpm.conf -D autostart=true autorestart=true 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 --memory=256 autostart=true autorestart=true user=www-data stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 stopsignal=INT
Kubernetes Manifests: Deployment and Service
With our Docker image ready, we define Kubernetes resources. A Deployment manages our application pods, ensuring the desired number of replicas are running and handling rolling updates. A Service provides a stable network endpoint for accessing the application.
We’ll use a Deployment to manage multiple replicas of our Laravel application. This allows for horizontal scaling and high availability. The Service will expose our application internally within the cluster, and we’ll typically use an Ingress controller for external access.
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-app
labels:
app: laravel
spec:
replicas: 3 # Start with 3 replicas
selector:
matchLabels:
app: laravel
template:
metadata:
labels:
app: laravel
spec:
containers:
- name: laravel-app
image: your-docker-registry/laravel-app:latest # Replace with your image
ports:
- containerPort: 9000
env:
- name: APP_ENV
value: "production"
- name: APP_KEY
valueFrom:
secretKeyRef:
name: laravel-secrets
key: APP_KEY
- name: DB_HOST
value: "mysql-service" # Assuming a MySQL service named mysql-service
- name: DB_PORT
value: "3306"
- name: DB_DATABASE
valueFrom:
secretKeyRef:
name: laravel-secrets
key: DB_DATABASE
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: laravel-secrets
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: laravel-secrets
key: DB_PASSWORD
# Add other environment variables as needed (e.g., Redis, SQS)
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /healthz # A simple health check endpoint in your Laravel app
port: 9000
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz # A readiness check endpoint
port: 9000
initialDelaySeconds: 5
periodSeconds: 10
# Define volumes for persistent storage if needed (e.g., for logs, uploads)
# volumes:
# - name: shared-storage
# emptyDir: {}
apiVersion: v1
kind: Service
metadata:
name: laravel-app-service
spec:
selector:
app: laravel
ports:
- protocol: TCP
port: 80
targetPort: 9000 # Port PHP-FPM is listening on inside the container
type: ClusterIP # Use ClusterIP for internal access, Ingress for external
We also need a Secret to securely store sensitive information like the APP_KEY and database credentials. This should be created separately using kubectl create secret or managed by a secrets management tool.
kubectl create secret generic laravel-secrets \ --from-literal=APP_KEY='your_laravel_app_key_here' \ --from-literal=DB_DATABASE='your_db_name' \ --from-literal=DB_USERNAME='your_db_user' \ --from-literal=DB_PASSWORD='your_db_password'
Scaling Strategies: Horizontal Pod Autoscaler (HPA)
To dynamically scale our Laravel application based on load, we employ the Horizontal Pod Autoscaler (HPA). The HPA automatically adjusts the number of pods in a deployment based on observed metrics like CPU utilization or custom metrics.
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: laravel-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: laravel-app
minReplicas: 2 # Minimum number of pods
maxReplicas: 10 # Maximum number of pods
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: 80 # Example for memory scaling
# - type: Pods # Example for scaling based on number of pods (less common for web apps)
# pods:
# metric:
# name: http_requests_per_second
# target:
# type: AverageValue
# averageValue: 1000 # Scale up if average requests per pod exceed 1000
For custom metrics (like requests per second), you’ll need a metrics server integration (e.g., Prometheus Adapter) and potentially an application-level metrics exporter.
Database and Cache Scaling Considerations
While Kubernetes excels at scaling stateless applications like our Laravel web servers, stateful components require separate strategies. For databases (e.g., MySQL, PostgreSQL), consider managed database services (AWS RDS, Google Cloud SQL) or Kubernetes operators like the Percona Operator for MySQL.
For caching (e.g., Redis, Memcached), a managed Redis service or a Redis Cluster deployed via an operator is recommended. If deploying Redis within Kubernetes, ensure you use a StatefulSet for stable network identities and persistent storage.
# Example of a basic Redis deployment using a StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
spec:
serviceName: "redis-headless"
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:6.2-alpine
ports:
- containerPort: 6379
name: redis
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
volumeMounts:
- name: redis-config-volume
mountPath: /usr/local/etc/redis
- name: redis-data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: redis-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
name: redis-headless
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
clusterIP: None # Headless service for StatefulSet
In your Laravel application’s configuration (e.g., config/database.php and config/cache.php), you would then point to the Kubernetes service name for Redis, such as redis-headless.your-namespace.svc.cluster.local.
Queue Workers and Background Jobs
Laravel’s queue workers are prime candidates for separate deployments. This allows them to scale independently of the web application and be configured with different resource limits. We can use a Deployment for queue workers, similar to the web app, but with a focus on long-running processes.
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-queue-worker
labels:
app: laravel-queue
spec:
replicas: 2 # Start with 2 worker pods
selector:
matchLabels:
app: laravel-queue
template:
metadata:
labels:
app: laravel-queue
spec:
containers:
- name: laravel-queue-worker
image: your-docker-registry/laravel-app:latest # Use the same app image
command: ["php", "artisan", "queue:work", "--tries=3", "--timeout=60", "--memory=256"]
env:
- name: APP_ENV
value: "production"
- name: APP_KEY
valueFrom:
secretKeyRef:
name: laravel-secrets
key: APP_KEY
- name: DB_HOST
value: "mysql-service"
- name: DB_PORT
value: "3306"
- name: DB_DATABASE
valueFrom:
secretKeyRef:
name: laravel-secrets
key: DB_DATABASE
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: laravel-secrets
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: laravel-secrets
key: DB_PASSWORD
# Add other environment variables as needed
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "1024Mi"
# Consider setting a higher restart policy for workers if appropriate
# restartPolicy: OnFailure
You can also implement an HPA for the queue workers based on queue length or other relevant metrics. This often requires integrating with a message queue system like Redis or SQS and exposing metrics about the queue size.
Monitoring and Logging
Effective monitoring and logging are paramount in a microservices architecture. For Kubernetes, consider a stack like Prometheus for metrics collection and Grafana for visualization. For logs, Elasticsearch, Fluentd, and Kibana (EFK stack) or Loki and Promtail are popular choices.
Ensure your Laravel application exposes relevant metrics. For example, you can use packages like laravel-prometheus-exporter to expose application-level metrics that can be scraped by Prometheus. For logs, configure Laravel’s logging to output to standard output (stdout) and standard error (stderr), which Kubernetes can then collect.
// In config/logging.php, set the default driver to 'stderr'
'default' => env('LOG_CHANNEL', 'stderr'),
// Or configure specific channels to output to stdout/stderr
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single'],
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => env('LOG_PATH', 'php://stderr'), // Direct to stderr
'level' => env('LOG_LEVEL', 'debug'),
],
// ... other channels
],
By directing logs to stdout/stderr, Fluentd or Promtail agents running on your Kubernetes nodes can easily capture and forward them to your centralized logging system.