Orchestrating Microservices with Docker Swarm and Laravel Octane: A High-Performance, Scalable Architecture for Modern Web Applications
Docker Swarm Initialization and Node Setup
To orchestrate our Laravel Octane microservices, Docker Swarm provides a robust and integrated solution. We’ll begin by initializing a Swarm on our manager node and then join worker nodes. This setup assumes you have Docker installed on all your machines.
On your designated manager node, execute the following command:
This command initializes a new Swarm and outputs a join token for worker nodes. It’s crucial to secure this token as it grants access to your Swarm.
docker swarm init --advertise-addr
After initialization, you’ll see output similar to this, including the command to join worker nodes:
Swarm initialized: current node (...) is now a manager.
To add a worker to this Swarm, run the following command:
docker swarm join --token :2377
To add a manager to this Swarm, run 'docker swarm join-token manager' and follow the instructions.
On each of your worker nodes, use the provided join token to connect them to the Swarm:
docker swarm join --token:2377
Verify the nodes are part of the Swarm by running this command on the manager node:
docker node ls
Containerizing Laravel Octane Microservices
Each microservice built with Laravel Octane needs a Dockerfile to define its image. For an Octane-powered application, the key is to ensure the Octane server is running correctly within the container. We’ll use a multi-stage build for efficiency.
Consider a microservice named ‘auth-service’. Its Dockerfile might look like this:
# Stage 1: Build dependencies
FROM php:8.2-fpm AS builder
WORKDIR /var/www/html
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
unzip \
libzip-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
nginx \
supervisor \
&& rm -rf /var/lib/apt/lists/*
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install gd pdo pdo_mysql zip
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
# Copy application code
COPY . .
# Install dependencies and optimize autoloader
RUN composer install --no-dev --optimize-autoloader
# Pre-register Octane
RUN php artisan octane:install --force
RUN php artisan octane:start --host=0.0.0.0 --port=8000 --workers=4 --max-requests=500 --watch=false
# Stage 2: Production image
FROM php:8.2-fpm
WORKDIR /var/www/html
# Install only necessary runtime dependencies
RUN apt-get update && apt-get install -y \
libzip4 \
libpng-dev \
libjpeg-dev \
libfreetype6 \
nginx \
supervisor \
&& rm -rf /var/lib/apt/lists/*
# Copy PHP extensions from builder stage if needed, or re-install minimal set
RUN docker-php-ext-install pdo pdo_mysql zip
# Copy application code from builder stage
COPY --from=builder /var/www/html /var/www/html
# Copy pre-compiled Octane assets
COPY --from=builder /var/www/html /var/www/html
# Copy Nginx configuration
COPY docker/nginx/app.conf /etc/nginx/sites-available/default
RUN ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
# Copy Supervisor configuration
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Expose port
EXPOSE 8000
# Start services
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
You’ll also need a docker/nginx/app.conf and docker/supervisor/supervisord.conf. The Nginx config will proxy requests to the Octane server, and Supervisor will manage the Octane process and Nginx.
Example docker/nginx/app.conf:
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
# This part is crucial: proxy to the Octane server
proxy_pass http://127.0.0.1:8000;
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;
proxy_connect_timeout 60;
proxy_send_timeout 60;
proxy_read_timeout 60;
}
location ~ /\.ht {
deny all;
}
}
Example docker/supervisor/supervisord.conf:
[supervisord] nodaemon=true user=root [program:nginx] command=/usr/sbin/nginx -g "daemon off;" autostart=true autorestart=true priority=10 stdout_logfile=/var/log/supervisor/nginx_stdout.log stderr_logfile=/var/log/supervisor/nginx_stderr.log [program:octane] command=php artisan octane:start --host=0.0.0.0 --port=8000 --workers=4 --max-requests=500 autostart=true autorestart=true priority=20 stdout_logfile=/var/log/supervisor/octane_stdout.log stderr_logfile=/var/log/supervisor/octane_stderr.log user=www-data
After creating the Dockerfile and configuration files for each microservice, build the Docker images:
# Navigate to the microservice directory cd /path/to/your/auth-service # Build the image docker build -t your-dockerhub-username/auth-service:latest .
Push these images to a Docker registry (e.g., Docker Hub, AWS ECR, Google GCR) so Swarm can pull them.
docker push your-dockerhub-username/auth-service:latest
Defining Services with Docker Compose and Swarm Deploy
Docker Swarm utilizes Docker Compose files (version 3+) for defining multi-container applications and their configurations. This file will specify how our microservices should be deployed, scaled, and networked within the Swarm.
Create a docker-compose.yml file for your Swarm. This example defines two services: auth-service and user-service, both running Laravel Octane.
version: '3.8'
services:
auth-service:
image: your-dockerhub-username/auth-service:latest
ports:
- "8001:80" # Expose on host port 8001
deploy:
replicas: 3 # Start with 3 replicas
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
networks:
- app-network
user-service:
image: your-dockerhub-username/user-service:latest
ports:
- "8002:80" # Expose on host port 8002
deploy:
replicas: 2
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
networks:
- app-network
# Example of a shared database service (e.g., MySQL)
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: your_root_password
MYSQL_DATABASE: app_db
MYSQL_USER: app_user
MYSQL_PASSWORD: app_password
volumes:
- db_data:/var/lib/mysql
networks:
- app-network
networks:
app-network:
driver: overlay
attachable: true
volumes:
db_data:
driver: local
Key elements in this docker-compose.yml:
image: Specifies the Docker image to use for the service.ports: Maps host ports to container ports. Note that Swarm will manage port distribution if multiple replicas are running on the same host.deploy: Contains Swarm-specific configurations like the number ofreplicas,update_configfor rolling updates, andrestart_policy.networks: Defines overlay networks, essential for inter-service communication in a multi-host Swarm.attachable: trueallows standalone containers to join these overlay networks if needed.volumes: For persistent data like databases.
To deploy these services to your Swarm, navigate to the directory containing your docker-compose.yml file on the manager node and run:
docker stack deploy -c docker-compose.yml my-app-stack
This command deploys the services defined in the Compose file as a “stack” named my-app-stack. Swarm will then pull the images and start the specified number of replicas for each service across your cluster nodes.
Service Discovery and Inter-Service Communication
Docker Swarm provides built-in DNS-based service discovery. When you define services in your docker-compose.yml and connect them to an overlay network (like app-network in the example), Swarm automatically assigns a virtual IP address and DNS entry for each service. Other containers on the same overlay network can reach a service using its service name.
For instance, the auth-service can communicate with the user-service by simply using the hostname user-service. Laravel’s configuration can be updated to reflect this.
In your Laravel application’s .env file for the auth-service, you would configure the user service endpoint like this:
# .env for auth-service APP_URL=http://auth-service:8000 # Or the host port if accessing externally USER_SERVICE_URL=http://user-service:80 # Assuming user-service exposes port 80 internally
And in the user-service‘s .env:
# .env for user-service APP_URL=http://user-service:8000 AUTH_SERVICE_URL=http://auth-service:80 # Assuming auth-service exposes port 80 internally
Your Laravel code can then access these services using these environment variables:
use Illuminate\Support\Facades\Http;
// In AuthServiceImpl.php or similar
public function getUserDetails($userId) {
$userServiceUrl = env('USER_SERVICE_URL');
$response = Http::get("{$userServiceUrl}/api/users/{$userId}");
return $response->json();
}
// In UserService.php or similar
public function authenticateUser($credentials) {
$authServiceUrl = env('AUTH_SERVICE_URL');
$response = Http::post("{$authServiceUrl}/api/login", $credentials);
return $response->json();
}
The overlay network ensures that traffic between services is routed efficiently and securely within the Swarm. For external access, you would typically use a load balancer (like HAProxy, Nginx, or a cloud provider’s LB) pointing to the published ports of your services, or deploy a dedicated ingress service within Swarm.
Scaling and Rolling Updates
Docker Swarm excels at managing the lifecycle of your services, including scaling and updates. To scale a service up or down, you can use the docker service scale command.
For example, to scale the auth-service to 5 replicas:
docker service scale my-app-stack_auth-service=5
To scale the user-service down to 1 replica:
docker service scale my-app-stack_user-service=1
Docker Swarm’s rolling update strategy, configured in the docker-compose.yml under deploy.update_config, allows you to update your services with zero downtime. When you update the Docker image for a service and redeploy the stack (using docker stack deploy -c docker-compose.yml my-app-stack again), Swarm will gradually replace old tasks with new ones according to the defined parallelism and delay.
The parallelism: 1 and delay: 10s in the example mean Swarm will update one container at a time, waiting 10 seconds between each update. This ensures that at least one instance of your service is always available.
You can monitor the status of your services and tasks using:
docker service ls docker service ps my-app-stack_auth-service
Monitoring and Health Checks
Robust monitoring is critical for any production system. Docker Swarm provides basic health checking capabilities. You can define health checks within your Dockerfile or directly in the docker-compose.yml.
A common approach is to have a dedicated health check endpoint in your Laravel application. For example, a route /health that returns a 200 OK status if the application is healthy.
In your Laravel routes (e.g., routes/api.php):
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Route;
Route::get('/health', function () {
// Add checks for database connectivity, external service availability, etc.
// For simplicity, we'll just return OK.
try {
// Example: Check database connection
DB::connection()->getPdo();
return response()->json(['status' => 'ok', 'message' => 'Application is healthy']);
} catch (\Exception $e) {
return response()->json(['status' => 'error', 'message' => 'Database connection failed'], 500);
}
});
Then, configure this health check in your docker-compose.yml:
services:
auth-service:
image: your-dockerhub-username/auth-service:latest
ports:
- "8001:80"
deploy:
replicas: 3
# ... other deploy options
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost:8000/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s # Give Octane time to start up
When a task fails its health check, Docker Swarm will automatically restart it. For more advanced monitoring, consider integrating with external tools like Prometheus and Grafana, which can scrape metrics from your services and provide comprehensive dashboards and alerting.
For logging, ensure your Supervisor configuration directs logs to files within the container. You can then use a log collection agent (like Fluentd, Logstash, or Filebeat) running as a Daemon Service in Swarm to aggregate logs from all nodes to a central location.