Orchestrating Microservices with Docker Swarm and Laravel Octane: A Scalable & Resilient Architecture for High-Traffic WordPress Headless
Docker Swarm: The Foundation for Scalable Microservices
For orchestrating microservices, Docker Swarm offers a compelling balance of simplicity and power, especially when compared to more complex alternatives like Kubernetes for certain use cases. Its integrated nature within the Docker ecosystem means less overhead and a gentler learning curve for teams already familiar with Docker. We’ll leverage Swarm’s declarative service model to define our application’s desired state, allowing Swarm to manage scaling, rolling updates, and service discovery.
A typical Swarm setup involves manager nodes and worker nodes. Manager nodes are responsible for cluster state and orchestration, while worker nodes execute the containers. For high availability, a minimum of three manager nodes is recommended. Here’s a basic initialization command for a manager node:
On the first manager node:
docker swarm init --advertise-addr
This command will output a `docker swarm join` command that you’ll use to onboard other manager and worker nodes. For instance, to join a worker node:
docker swarm join --token:2377
Laravel Octane: Turbocharging PHP for Headless WordPress
Traditional PHP applications, including WordPress, suffer from high latency due to the overhead of booting the framework on every request. Laravel Octane revolutionizes this by keeping your application’s processes alive in the background, serving requests at blistering speeds. This is crucial for a headless WordPress setup where the API layer needs to be highly performant to serve front-end applications.
Octane supports multiple application servers, including Swoole and RoadRunner. For production environments, RoadRunner is often preferred due to its robust features and active development. To integrate Octane with RoadRunner, you’ll need to install the RoadRunner binary and configure it.
First, ensure you have Laravel Octane installed in your Laravel project:
composer require Laravel/octane
Next, publish Octane’s configuration and select RoadRunner as your server:
php artisan octane:install --roadrunner php artisan vendor:publish --tag=octane-config
The `config/octane.php` file will now contain RoadRunner-specific configurations. You’ll also need a `.rr.yaml` file in your project root to configure RoadRunner itself. A minimal configuration might look like this:
version: '2.0' rpc: listen: tcp://127.0.0.1:6001 server: command: "php artisan octane:server --roadrunner" relay: "pipes" num_workers: 4 # Adjust based on your server's CPU cores max_jobs: 1000 http: address: ":8000" max_request_size: 10485760 # 10MB logs: mode: development
Dockerizing the Headless WordPress API
To deploy our Octane-powered Laravel API on Docker Swarm, we need a Dockerfile. This Dockerfile will build an image that includes our Laravel application, its dependencies, and the necessary setup to run Octane with RoadRunner.
# Use an official PHP image with extensions required by Laravel and Swoole/RoadRunner
FROM php:8.2-fpm
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
unzip \
libzip-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libicu-dev \
libonig-dev \
libxml2-dev \
zip \
&& 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 mbstring zip pdo pdo_mysql bcmath intl opcache
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy application files
COPY . /var/www/html
# Install Composer dependencies
RUN composer install --no-dev --optimize-autoloader
# Install RoadRunner binary (example for Linux x86_64)
RUN curl -Ls https://github.com/spiral/roadrunner/releases/download/v2.15.0/roadrunner-2.15.0-linux-amd64.tar.gz | tar -xz -C /usr/local/bin/
# Copy RoadRunner configuration
COPY .rr.yaml /etc/roadrunner/rr.yaml
# Expose port
EXPOSE 8000
# Command to run RoadRunner
CMD ["/usr/local/bin/rr", "serve", "--config=/etc/roadrunner/rr.yaml"]
Build this image locally first to ensure it functions correctly:
docker build -t your-dockerhub-username/headless-api:latest .
Docker Swarm Service Definition
With our Docker image ready, we can define a Docker Swarm service. This involves creating a `docker-compose.yml` file that describes the service, its image, ports, scaling, and any dependencies like a database.
Here’s an example `docker-compose.yml` for our headless API service:
version: '3.8'
services:
api:
image: your-dockerhub-username/headless-api:latest
ports:
- "8000:8000" # Map host port 8000 to container port 8000
deploy:
replicas: 3 # Start with 3 replicas for high availability
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
environment:
# Database credentials and other environment variables
DB_HOST: mysql_db
DB_PORT: 3306
DB_DATABASE: wordpress
DB_USERNAME: user
DB_PASSWORD: password
APP_ENV: production
APP_DEBUG: false
networks:
- app-network
# Example MySQL service (for demonstration, use a managed DB in production)
mysql_db:
image: mysql:8.0
volumes:
- mysql_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: wordpress
MYSQL_USER: user
MYSQL_PASSWORD: password
networks:
- app-network
volumes:
mysql_data:
networks:
app-network:
driver: overlay
attachable: true
To deploy this service to your Swarm cluster:
docker stack deploy -c docker-compose.yml headless_stack
This command will create a stack named `headless_stack` containing our `api` and `mysql_db` services. Swarm will ensure that 3 replicas of the `api` service are running and accessible on port 8000 of any node in the Swarm. The `overlay` network allows containers across different nodes to communicate seamlessly.
Load Balancing and Ingress with Traefik
For robust load balancing and SSL termination, we’ll integrate Traefik as our reverse proxy and ingress controller. Traefik can dynamically discover Docker Swarm services and configure routing rules automatically.
First, deploy Traefik as a Swarm service. You’ll typically use a `docker-compose.yml` for Traefik itself. Ensure it’s configured to listen on ports 80 and 443 on all nodes.
version: '3.8'
services:
traefik:
image: traefik:v2.10
command:
- "--api.insecure=true" # For dashboard access, remove in production or secure it
- "--providers.docker=true"
- "--providers.docker.swarmmode=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Add Let's Encrypt configuration here for SSL
# - "--certificatesresolvers.myresolver.acme.tlschallenge=true"
# - "--certificatesresolvers.myresolver.acme.email=your-email@example.com"
# - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
- "8080:8080" # Traefik dashboard
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# - ./letsencrypt:/letsencrypt # Uncomment for Let's Encrypt
networks:
- app-network
deploy:
placement:
constraints: [node.role == manager] # Or distribute across all nodes
restart_policy:
condition: on-failure
networks:
app-network:
external: true # Use the existing network
Deploy Traefik:
docker stack deploy -c traefik-compose.yml traefik_stack
Now, we need to tell Traefik how to route traffic to our `api` service. We do this by adding labels to the `api` service definition in our `docker-compose.yml`:
version: '3.8'
services:
api:
image: your-dockerhub-username/headless-api:latest
ports:
- "8000:8000"
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
environment:
DB_HOST: mysql_db
DB_PORT: 3306
DB_DATABASE: wordpress
DB_USERNAME: user
DB_PASSWORD: password
APP_ENV: production
APP_DEBUG: false
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.yourdomain.com`)" # Your API domain
- "traefik.http.routers.api.entrypoints=websecure" # Use websecure for HTTPS
# - "traefik.http.routers.api.tls.certresolver=myresolver" # Uncomment for Let's Encrypt
- "traefik.http.services.api.loadbalancer.server.port=8000" # Port RoadRunner listens on inside container
- "traefik.http.routers.api.service=api"
- "traefik.http.routers.api.middlewares=api-strip-prefix" # Optional: if your API needs a prefix removed
- "traefik.http.middlewares.api-strip-prefix.stripprefix.prefixes=/api" # Example prefix
networks:
- app-network
mysql_db:
image: mysql:8.0
volumes:
- mysql_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: wordpress
MYSQL_USER: user
MYSQL_PASSWORD: password
networks:
- app-network
volumes:
mysql_data:
networks:
app-network:
driver: overlay
attachable: true
After updating `docker-compose.yml` and redeploying the stack (`docker stack deploy -c docker-compose.yml headless_stack`), Traefik will automatically pick up the `api` service and route traffic from `api.yourdomain.com` to the running Octane containers. For production, ensure you configure SSL with Let’s Encrypt within Traefik’s configuration.
Monitoring and Health Checks
Maintaining a resilient system requires robust monitoring. Docker Swarm provides basic health checks, and Octane/RoadRunner also have mechanisms to report their status. We can integrate these into our deployment.
In the `docker-compose.yml` for the `api` service, add a `healthcheck` directive:
services:
api:
# ... other configurations ...
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8000/health"] # Assuming you have a /health endpoint in Laravel
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
# ... rest of the service definition ...
You’ll need to create a simple route in your Laravel application (e.g., in `routes/api.php` or `routes/web.php` if not using API routes exclusively) that returns a 200 OK response for the `/health` endpoint. This allows Swarm to determine if a container is truly healthy and ready to serve traffic.
// In a controller or directly in routes/api.php
Route::get('/health', function () {
// Optionally, check database connection or other critical services
try {
DB::connection()->getPdo();
return response()->json(['status' => 'ok']);
} catch (\Exception $e) {
return response()->json(['status' => 'error', 'message' => 'Database connection failed'], 503);
}
});
For more advanced monitoring, consider integrating Prometheus and Grafana. Traefik can expose metrics, and you can configure exporters for your Laravel application (e.g., using a custom Prometheus client or by scraping logs). Tools like Datadog or New Relic can also provide deep insights into application performance and infrastructure health.
Conclusion and Further Considerations
This architecture provides a scalable, resilient, and high-performance foundation for a headless WordPress API. By combining Docker Swarm’s orchestration capabilities with Laravel Octane’s speed and Traefik’s intelligent routing, you can build an API capable of handling significant traffic loads.
Key considerations for production:
- Database Management: For production, use a managed database service (AWS RDS, Google Cloud SQL, etc.) instead of running MySQL within Swarm.
- Caching: Implement Redis or Memcached for object caching and session management to further reduce database load and improve response times.
- CI/CD: Automate your build, test, and deployment pipeline using tools like GitLab CI, GitHub Actions, or Jenkins.
- Security: Regularly update Docker images, secure Traefik with SSL, and implement proper authentication/authorization for your API.
- Logging: Centralize logs from all containers using a logging driver (e.g., Fluentd, Logstash) and a centralized logging system (e.g., Elasticsearch, Splunk).