Orchestrating Microservices with Docker Swarm: A Practical Guide to Scalable PHP & Laravel Deployments
Setting Up a Docker Swarm Cluster
Before orchestrating microservices, we need a functional Docker Swarm cluster. This guide assumes you have at least two machines (physical or virtual) with Docker installed. One will act as the manager node, and the others as worker nodes.
On the designated manager node, initialize the swarm:
docker swarm init --advertise-addr
This command will output a docker swarm join command. Execute this command on each worker node to add them to the swarm. For example:
docker swarm join --token SWMTKN-1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx x.x.x.x:2377
Verify the cluster status on the manager node:
docker node ls
Defining Microservices with Docker Compose
We’ll define our PHP/Laravel microservices using a docker-compose.yml file. For a typical Laravel application, this would include services for the web application, a database (e.g., MySQL or PostgreSQL), and potentially a cache (e.g., Redis).
version: '3.8'
services:
app:
image: php:8.2-fpm-alpine
container_name: my_laravel_app
volumes:
- ./src:/var/www/html
working_dir: /var/www/html
networks:
- app-network
depends_on:
- db
- redis
environment:
DB_HOST: db
DB_PORT: 3306
DB_DATABASE: laravel_db
DB_USERNAME: user
DB_PASSWORD: password
REDIS_HOST: redis
REDIS_PORT: 6379
deploy:
replicas: 3 # Initial desired replicas
restart_policy:
condition: on-failure
nginx:
image: nginx:alpine
container_name: my_nginx_proxy
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./src:/var/www/html # Mount app code for Nginx to serve static assets
networks:
- app-network
depends_on:
- app
deploy:
replicas: 1
restart_policy:
condition: on-failure
db:
image: mysql:8.0
container_name: my_mysql_db
volumes:
- db_data:/var/lib/mysql
networks:
- app-network
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: laravel_db
MYSQL_USER: user
MYSQL_PASSWORD: password
deploy:
replicas: 1
restart_policy:
condition: on-failure
redis:
image: redis:alpine
container_name: my_redis_cache
networks:
- app-network
deploy:
replicas: 1
restart_policy:
condition: on-failure
networks:
app-network:
driver: overlay
volumes:
db_data:
Key points:
- We use the
overlaynetwork driver, which is essential for multi-host Swarm communication. - The
deploysection is crucial for Swarm. It defines the desired state, including the number of replicas (containers) for each service. - Environment variables are used to configure the application and its dependencies.
- Volumes are defined for persistent data (like the database) and for mounting application code.
Building and Pushing Docker Images
For production, it’s best practice to build your application image and push it to a container registry (e.g., Docker Hub, AWS ECR, Google Container Registry). This ensures consistent deployments across your Swarm nodes.
Create a Dockerfile for your Laravel application:
FROM php:8.2-fpm-alpine
RUN apk add --no-cache \
nginx \
supervisor \
git \
zip \
unzip \
icu-dev \
libzip-dev \
libpng-dev \
freetype-dev \
jpeg-dev \
libjpeg-turbo-dev \
libwebp-dev \
libintl-dev \
libxml2-dev \
oniguruma-dev \
postgresql-dev \
# Add other necessary packages
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install intl zip pdo pdo_mysql # Adjust pdo_mysql if using PostgreSQL
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
COPY ./src /var/www/html
COPY ./docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY ./docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
WORKDIR /var/www/html
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html \
&& composer install --no-dev --optimize-autoloader \
&& php artisan cache:clear \
&& php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
And a corresponding supervisord.conf to manage PHP-FPM and Nginx:
[supervisord] nodaemon=true user=root [program:php-fpm] command=/usr/local/sbin/php-fpm -D autostart=true autorestart=true priority=10 stdout_logfile=/var/log/supervisor/php-fpm.log stderr_logfile=/var/log/supervisor/php-fpm.err.log [program:nginx] command=/usr/sbin/nginx -g "daemon off;" autostart=true autorestart=true priority=20 stdout_logfile=/var/log/supervisor/nginx.log stderr_logfile=/var/log/supervisor/nginx.err.log
Build the image:
docker build -t your-dockerhub-username/my-laravel-app:latest .
Tag and push to your registry:
docker push your-dockerhub-username/my-laravel-app:latest
Deploying Services to Docker Swarm
Now, we’ll deploy our services to the Swarm. We’ll modify the docker-compose.yml to use our custom image and remove the container_name as Swarm assigns unique IDs.
version: '3.8'
services:
app:
image: your-dockerhub-username/my-laravel-app:latest # Use your custom image
ports:
- "8000:80" # Expose app port for external access if needed, or rely on ingress
networks:
- app-network
depends_on:
- db
- redis
environment:
DB_HOST: db
DB_PORT: 3306
DB_DATABASE: laravel_db
DB_USERNAME: user
DB_PASSWORD: password
REDIS_HOST: redis
REDIS_PORT: 6379
deploy:
replicas: 3
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
nginx:
image: nginx:alpine
ports:
- "80:80" # Publicly accessible port
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- app-network
depends_on:
- app
deploy:
replicas: 1
update_config:
parallelism: 1
restart_policy:
condition: on-failure
db:
image: mysql:8.0
networks:
- app-network
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: laravel_db
MYSQL_USER: user
MYSQL_PASSWORD: password
volumes:
- db_data:/var/lib/mysql
deploy:
replicas: 1
restart_policy:
condition: on-failure
redis:
image: redis:alpine
networks:
- app-network
deploy:
replicas: 1
restart_policy:
condition: on-failure
networks:
app-network:
driver: overlay
volumes:
db_data:
Deploy the stack to your Swarm cluster:
docker stack deploy -c docker-compose.yml my_laravel_app_stack
This command will create a new “stack” named my_laravel_app_stack and deploy all the defined services across your Swarm nodes. Swarm will ensure the desired number of replicas for each service are running.
Configuring Nginx for Load Balancing
The Nginx service will act as a reverse proxy and load balancer for our PHP application. We need to configure it to forward requests to the available instances of the app service. Swarm’s DNS resolution makes this straightforward.
# ./nginx.conf
upstream php_app {
# Swarm service name 'app' will resolve to the IPs of its running tasks
server app:9000;
}
server {
listen 80;
index index.php index.html;
root /var/www/html/public; # Adjust if your public directory is different
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files /dev/null =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php_app; # Use the upstream name defined above
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
# Serve static assets directly from the mounted volume
location ~* \.(css|js|jpg|jpeg|gif|png|svg|ico|webp)$ {
expires 1d;
add_header Cache-Control "public";
# Ensure this path matches where your static assets are served from
root /var/www/html/public;
}
}
In this Nginx configuration:
- The
upstream php_appblock defines a group of servers. When you use the service nameapp(which is the service name in ourdocker-compose.yml) as a hostname, Docker’s embedded DNS will resolve it to the IP addresses of all running containers (tasks) for that service. Nginx will then round-robin requests to these IPs. - We’re passing requests to the PHP-FPM service on port 9000.
- Static assets are configured to be served directly by Nginx for better performance.
Scaling and Health Checks
Docker Swarm provides built-in mechanisms for scaling and health monitoring.
To scale the application service up or down, update the replicas count in your docker-compose.yml and redeploy:
# Edit docker-compose.yml, change replicas: 3 to replicas: 5 docker stack deploy -c docker-compose.yml my_laravel_app_stack
Swarm will automatically adjust the number of running containers for the app service. The update_config in the deploy section controls how these updates are rolled out, minimizing downtime.
For more robust health checking, you can add a healthcheck directive to your app service in the docker-compose.yml. This allows Swarm to detect unhealthy containers and replace them.
# ... inside the 'app' service definition ...
deploy:
replicas: 3
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] # Assuming you have a /health endpoint
interval: 30s
timeout: 10s
retries: 3
start_period: 60s # Give the container time to start up
You would need to implement a simple /health route in your Laravel application that returns a 200 OK status code.
Managing Swarm Services
Here are some essential commands for managing your deployed stack:
- View running services:
docker service ls - View tasks (containers) for a service:
docker service ps my_laravel_app_stack_app - View logs for a service:
docker service logs my_laravel_app_stack_app - Scale a service manually:
docker service scale my_laravel_app_stack_app=5 - Update a service (e.g., with a new image):
docker service update --image your-dockerhub-username/my-laravel-app:new-tag my_laravel_app_stack_app - Remove the entire stack:
docker stack rm my_laravel_app_stack
This setup provides a solid foundation for deploying scalable PHP and Laravel applications using Docker Swarm, leveraging its orchestration capabilities for resilience and manageability.