Scaling Laravel with Docker Swarm: From Local to Production

Deploying and scaling Laravel applications using Docker Swarm for seamless transitions from development to production. Cluster orchestration, load balancing, and high-availability best practices for robust, scalable Laravel deployments.

Building on Docker fundamentals, this article explains Docker Swarm's role in container orchestration for production environments. It covers Swarm concepts, guides through deploying a multi-service Laravel application to a Swarm cluster, and details Swarm's built-in load balancing, routing mesh, and crucial production considerations like shared storage and secrets management.

Introducing Docker Swarm: Container Orchestration for Production

Docker Swarm is Docker's native solution for container orchestration, allowing you to manage a cluster of Docker machines (nodes) as a single, virtual Docker Engine. It's designed for deploying, scaling, and managing containerized applications across multiple machines, providing high availability and fault tolerance.

Docker Swarm adds features like:

  • Service replication

  • Load balancing (built-in routing mesh)

  • High availability

  • Rolling updates

Why use Docker Swarm over just Docker Compose?

While Docker Compose is perfect for local development on a single machine, it's not designed for production environments where you need:

  • Fault Tolerance: What happens if the single server running your Docker Compose setup goes down?

  • Scalability: How do you handle increased traffic by adding more servers?

  • High Availability: How do you ensure your application remains accessible even during maintenance or failures?

Docker Swarm addresses these challenges by allowing you to create a cluster of nodes (physical or virtual machines) where your services are distributed and managed automatically.

Docker Swarm vs. Kubernetes (K8s)

Feature

Docker Swarm

Kubernetes (K8s)

Complexity

Simple, easy to get started

Complex, steep learning curve

Setup

Quick and straightforward

Longer and more involved

Integration

Native with Docker; tightly integrated with Docker CLI

Broader Ecosystem; more components to manage (kube-api, kube-scheduler, kube-controller-manager, kubelet, kube-proxy, etcd)

Use Case

Simpler orchestration needs, smaller clusters, rapid deployment

Large-scale, complex deployments, advanced features, enterprise-grade orchestration

Key Swarm Concepts:

  • Node: A Docker engine instance participating in the Swarm.

    • Manager Node: Manages the Swarm, dispatches tasks, maintains the Swarm state, and handles orchestration. For high availability, you typically have multiple manager nodes (e.g., 3 or 5).

    • Worker Node: Runs the containerized services (tasks) assigned by the manager nodes.

  • Service: A definition of the tasks to be executed in the Swarm. This is where you define which Docker image to use, the number of replicas, ports, volumes, etc. A service is very similar to what you define in a docker-compose.yml file.

  • Task: A running instance of a service. Each task runs within a Docker container.

  • Stack: A group of interrelated services that make up an application (like our Laravel app with Nginx, PHP, MySQL, PhpMyAdmin). A stack is essentially a docker-compose.yml file deployed to a Swarm.

Deploying the Laravel Stack to Docker Swarm (Practical Example)

To follow this example, you'll need at least two machines (virtual machines like those from VirtualBox, VMware, or cloud instances from AWS EC2, GCP, Azure, etc.) with Docker installed on each.

Let's assume you have two machines:

  • manager1 (IP: 192.168.1.100)

  • worker1 (IP: 192.168.1.101)

Project Structure:

The project structure for Docker Swarm deployment is the same as for Docker Compose, assuming your custom Dockerfile for PHP and NGINX config are within the docker/ folder.

laravel-cms/
├── app/                  # Laravel project files
├── docker/
│   ├── nginx/
│   │   └── default.conf  # NGINX config
│   └── php/
│       └── Dockerfile    # PHP environment for Laravel
├── docker-compose.yml    # Orchestration file (used as stack file)

Step 1: Initialize the Docker Swarm (on manager1)

Choose one machine to be your manager node (e.g., manager1). Run the following command, replacing <MANAGER_IP> with the actual IP address of your manager node:

# On manager1 (e.g., 192.168.1.100)
docker swarm init --advertise-addr 192.168.1.100

This command will output instructions on how to add worker nodes to the Swarm.

Step 2: Add Worker Nodes to the Swarm (on worker1)

Copy the docker swarm join command from the output of docker swarm init and run it on your worker node(s).

# On worker1 (e.g., 192.168.1.101)
docker swarm join --token SWMTKN-1-xxxxxxxxxxxxxxxxxxxxxxxxx 192.168.1.100:2377

You should see a message like This node joined a swarm as a worker..

Step 3: Verify Swarm Nodes (on manager1)

On your manager node, you can check the status of your Swarm:

# On manager1
docker node ls

You should see both manager1 and worker1 listed, with manager1 as "Leader" and worker1 as "Ready" and "Active".

Step 4: Prepare Docker Image for Swarm Deployment (Crucial)

Since Swarm distributes containers across nodes, all nodes need access to the Docker images. For custom images (like our PHP service for Laravel), you must build and push them to a Docker Registry (like Docker Hub or a private registry) so all Swarm nodes can pull them.

First, build your custom PHP application image and tag it for your Docker registry:

# In your laravel-cms/ directory
docker build -t skarnov/laravel-cms-app:latest ./docker/php
docker push skarnov/laravel-cms-app:latest

Then, update your docker-compose.yml to use this pre-built image instead of building it:

# docker-compose.yml for Docker Swarm Deployment (updated for image usage)
version: '3.8'

services:
  app:
    image: skarnov/laravel-cms-app:latest # Use the pre-built image from Docker Hub
    volumes:
      # In production, for application code, prefer building into image or
      # using shared storage (NFS/GlusterFS/Cloud Volumes) instead of bind mounts.
      - ./app:/var/www/html
    deploy:
      replicas: 3 # Scale the application service to 3 replicas
      restart_policy:
        condition: on-failure # Restart if container fails
      update_config:
        parallelism: 1
        delay: 10s
        failure_action: rollback # Roll back if an update fails
        monitor: 60s # Time to monitor a service for failures after a task update
      resources: # Define resource limits (crucial for production)
        limits:
          cpus: '0.50' # 0.5 CPU core
          memory: 512M
        reservations:
          cpus: '0.25' # Reserve 0.25 CPU core
          memory: 256M
      healthcheck: # Ensure the PHP-FPM service is healthy
        test: ["CMD", "php-fpm", "-t"]
        interval: 10s
        timeout: 5s
        retries: 3
    networks:
      - app-network

  web:
    image: nginx:stable-alpine
    ports:
      - "80:80" # Publish port 80 to the outside world
    volumes:
      # Similar note on volumes as 'app' service
      - ./app:/var/www/html
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app
    deploy:
      # Pin NGINX to manager nodes for simplified external access (optional, depends on setup)
      placement:
        constraints: [node.role == manager]
      replicas: 1 # Typically one Nginx if using external LB, or more for high availability on managers
      restart_policy:
        condition: on-failure
      healthcheck: # Ensure Nginx is healthy
        test: ["CMD-SHELL", "curl --fail http://localhost/ || exit 1"]
        interval: 10s
        timeout: 5s
        retries: 3
    networks:
      - app-network

  db:
    image: mariadb:10.6
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: laravel
      MYSQL_USER: laravel
      MYSQL_PASSWORD: secret
    volumes:
      # For production, use shared storage (NFS, cloud volumes) or a managed DB service
      - db_data:/var/lib/mysql
    deploy:
      replicas: 1 # Database typically runs as a single replica or uses external managed service
      restart_policy:
        condition: on-failure
    networks:
      - app-network
    # secrets: # Use Docker secrets for production passwords
    #   - mysql_password

  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    ports:
      - "8080:80"
    environment:
      PMA_HOST: db
      MYSQL_ROOT_PASSWORD: root
    depends_on:
      - db
    deploy:
      replicas: 1 # Typically a single replica for admin tools
      restart_policy:
        condition: on-failure
    networks:
      - app-network

# Define networks
networks:
  app-network:
    driver: overlay # Use overlay network for multi-host communication in Swarm

# Define volumes for persistent data (for local testing, replace with shared storage in prod)
volumes:
  db_data:

# secrets: # Define secrets at the top level
#   mysql_password:
#     external: true

NGINX Load Balancing Config (docker/nginx/default.conf)

Ensure your default.conf uses the service name (app) as an upstream to allow NGINX to forward requests across the PHP-FPM replicas managed by Swarm:

# docker/nginx/default.conf
upstream laravel-upstream {
    server app:9000; # 'app' is the service name, Docker Swarm's DNS will resolve it to all PHP-FPM replicas
}

server {
    listen 80;
    index index.php index.html;
    server_name localhost;
    root /var/www/html/public;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass laravel-upstream; # Use the upstream definition
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_index index.php;
    }

    location ~ /\.ht {
        deny all;
    }
}

Step 5: Deploy Your Laravel Stack to the Swarm

Now, you'll use docker stack deploy. Make sure you are in the laravel-cms/ directory on your manager node (or a machine configured to connect to the Swarm manager).

docker stack deploy -c docker-compose.yml laravel_stack

  • docker stack deploy: The command to deploy a stack.

  • -c docker-compose.yml: Specifies your stack definition file.

  • laravel_stack: This is the name of your stack. All services in this stack will be prefixed with laravel_stack_ (e.g., laravel_stack_nginx, laravel_stack_app).

Docker Swarm will now parse your docker-compose.yml, create the services, and distribute the tasks (containers) across the available nodes in the Swarm.

Step 6: Verify Stack Deployment and Service Status

# On manager1
docker stack ls # List deployed stacks
docker stack services laravel_stack # List services within your stack
docker service ps laravel_stack_app # See which nodes individual tasks are running on

You'll see output showing the services, the number of replicas, and on which nodes they are running. Swarm will try to distribute them evenly if possible.

Step 7: Access Your Services

Since Nginx is exposing port 80, you can now access your Laravel application from any node's IP address within the Swarm:

  • Laravel Application: http://<ANY_NODE_IP>:80 (e.g., http://192.168.1.100:80 or http://192.168.1.101:80). Swarm's routing mesh will ensure the request reaches a running Nginx container.

  • PhpMyAdmin: http://<ANY_NODE_IP>:8080

Step 8: Scale Your Services

One of Swarm's biggest advantages is easy scaling. Let's scale the app service to 3 replicas (as defined in docker-compose.yml, but you can change it on the fly):

# On manager1
docker service scale laravel_stack_app=3

Now, check the service status again:

docker stack services laravel_stack
docker service ps laravel_stack_app

You'll see three instances of PHP-FPM for your application, distributed across your nodes. Docker Swarm automatically handles load balancing incoming requests across these replicas.

Step 9: Remove Your Stack and Swarm

When you're done, you can tear down the entire stack and remove the nodes from the Swarm.

First, remove the stack:

# On manager1
docker stack rm laravel_stack

This will stop and remove all services and their containers belonging to the laravel_stack stack.

Next, remove the worker nodes from the Swarm:

# On worker1
docker swarm leave

Finally, leave the Swarm on the manager node (or force it if it's the last manager):

# On manager1
docker swarm leave --force # Use --force if it's the only manager or you want to destroy the swarm

Important Considerations for Production Swarm Deployments:

To make this a more robust production-ready setup, here are some additional critical considerations:

  • Shared Storage for Persistent Data: For stateful services like databases (e.g., MySQL), local Docker volumes are tied to a specific node. If that node fails, your data is at risk. In a production Swarm, you must use a shared storage solution that all nodes can access. Common options include:

    • Network File System (NFS): A traditional shared file system.

    • Distributed File Systems (e.g., GlusterFS, Ceph): Provide high availability and scalability.

    • Cloud-Specific Persistent Volumes: (e.g., AWS EBS with a CSI driver, Google Cloud Persistent Disk, Azure Disk Storage) are managed by the cloud provider and can be re-attached to other instances.

    • Managed Database Services: For databases, the strongest recommendation for production is often to use a managed service (e.g., AWS RDS, Azure Database for MySQL, Google Cloud SQL) instead of running the database directly in Swarm. This offloads backups, replication, and scaling complexities.

  • Secrets Management: Never hardcode sensitive information (database credentials, API keys) directly in your docker-compose.yml or Dockerfile. Docker Swarm has a built-in secrets management system for this:

    1. Create a secret:

      echo "your_super_secret_db_password" | docker secret create mysql_password -
      
    2. Reference in docker-compose.yml:

      # ... inside services:db:environment (or other services needing the secret)
      MYSQL_PASSWORD: /run/secrets/mysql_password
      # ...
      secrets:
        - mysql_password
      
    3. Define secrets at the top level:

      # ... after volumes:
      secrets:
        mysql_password:
          external: true # Indicates the secret is already created externally
      
  • Ingress Routing and External Load Balancing: While Swarm's routing mesh handles internal load balancing, for external traffic, you often need a dedicated ingress solution:

    • Edge Load Balancer: Place a cloud load balancer (e.g., AWS ELB, Azure Load Balancer) or a dedicated reverse proxy (like Nginx, HAProxy, or Traefik) in front of your Swarm manager nodes to distribute incoming requests to your services.

    • Swarm's Ingress Network: Services exposing ports (ports:) participate in Swarm's ingress routing mesh, making them accessible on any node's exposed port. However, a dedicated load balancer provides more advanced features (SSL termination, WAF, etc.).

  • Health Checks (healthcheck): Define how Docker should determine if a service's container is healthy. This ensures Swarm only routes traffic to truly ready applications and automatically replaces unhealthy ones. (Examples already included in the docker-compose.yml for app and web services.)

  • Resource Constraints: Define CPU and memory limits for your services to prevent a single service from consuming all node resources. (Examples already included in the docker-compose.yml for app service.)

  • Logging and Monitoring: In a distributed environment, logs are scattered across nodes. Implement centralized logging and monitoring:

    • Logging: Use a logging driver (e.g., fluentd, syslog) in Docker to send container logs to a centralized log management system (e.g., ELK Stack (Elasticsearch, Logstash, Kibana), Splunk, Datadog).

    • Monitoring: Use tools like Prometheus for metrics collection and Grafana for visualization. Docker provides metrics endpoints for its daemon and containers.

  • Rolling Updates and Rollbacks: Swarm's update_config allows fine-grained control over how service updates are performed (e.g., delay between updates, failure action). This enables zero-downtime deployments. (Examples already included in the docker-compose.yml for app service.)

Orchestration

Orchestration is the automated management of containers across multiple machines. Think of it like a smart manager that knows how to:

  • Deploy applications (launch containers across available nodes).

  • Scale services (increase or decrease replicas based on demand).

  • Monitor container health (detect crashed containers and restart/reschedule them).

  • Route traffic (send traffic only to healthy, running containers via the Routing Mesh).

  • Manage updates (perform rolling updates and rollbacks).

Tools That Do Orchestration

Tool

Notes

Docker Swarm

Native to Docker, simple, and easy to use. Ideal for simpler, smaller clusters.

Kubernetes (K8s)

Powerful, complex, and the industry standard for large-scale, enterprise-grade container orchestration.

Nomad (by HashiCorp)

A lightweight alternative to Kubernetes, often used for diverse workloads (containers, VMs, batch jobs).

Rancher

A complete software stack for managing and deploying Kubernetes clusters. Provides a UI and management layer.

Conclusion on Docker Swarm

Docker Swarm provides a straightforward and powerful way to orchestrate your containerized applications across multiple hosts, moving beyond single-machine development to a highly available and scalable production setup. By understanding its core concepts and leveraging the familiar docker-compose.yml syntax, you can efficiently deploy, manage, and scale your Laravel application (or any multi-service application) in a clustered environment, ensuring resilience and performance. Incorporating the advanced considerations like shared storage, secrets management, and robust monitoring transforms your Swarm deployment into a truly production-grade solution.