Zero Downtime Deploy Laravel Docker with Traefik

Avatar
M

Maksudur Rahman

Software Engineer•
1Views
5mRead
0Reactions

You need to ship a new Laravel 11 version while users keep browsing your site. The classic problem is a brief 502 or 504 when the old container stops and the new one starts. This guide shows a zero downtime deploy Laravel Docker workflow that keeps every request alive.

TL;DR / Quick Summary

  • Dockerfile builds a stateless Laravel image.

  • docker-compose.yml defines healthchecks and a shared volume for sessions.

  • Traefik v3 routes traffic using rolling updates (canary style).

  • Deploy with docker stack deploy – Swarm handles the rollout.

  • Verify with curl -I and the Traefik dashboard.

Prerequisites

  • Laravel 11 project (PHP 8.3).

  • Docker Engine 24 and Docker Compose 2 installed on the host.

  • A Swarm manager node (docker swarm init).

  • Traefik 3 binary or Docker image.

  • DNS entry pointing to the host (or use /etc/hosts for testing).

Dockerfile (path: Dockerfile)

FROM php:8.3-fpm-alpine AS base
WORKDIR /var/www/html

# System deps
RUN apk add --no-cache \
    git \
    unzip \
    libpng-dev \
    libzip-dev \
    oniguruma-dev && \
    docker-php-ext-install pdo_mysql zip exif pcntl bcmath gd && \
    php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
    php composer-setup.php --install-dir=/usr/local/bin --filename=composer && \
    rm composer-setup.php

# Copy composer files first for cache‑friendly layers
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction

# Copy the rest of the app
COPY . .

# Permissions for storage & bootstrap/cache
RUN chown -R www-data:www-data storage bootstrap/cache

EXPOSE 9000
CMD ["php-fpm"]

The image is deliberately thin – all state lives outside the container.

docker-compose.yml (path: docker-compose.yml)

version: "3.9"
services:
  app:
    build: .
    image: myorg/laravel-app:latest
    deploy:
      mode: replicated
      replicas: 2
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first   # start new container before stopping old one
      restart_policy:
        condition: on-failure
    environment:
      - APP_ENV=production
      - APP_KEY=${APP_KEY}
      - SESSION_DRIVER=redis
      - CACHE_DRIVER=redis
    volumes:
      - shared-sessions:/var/www/html/storage/framework/sessions
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 15s
      timeout: 3s
      retries: 3
    networks:
      - web

  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
    networks:
      - web

  traefik:
    image: traefik:v3.0
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.file.directory=/etc/traefik/dynamic"
      - "--entrypoints.web.address=:80"
      - "--api.dashboard=true"
    ports:
      - "80:80"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik/dynamic:/etc/traefik/dynamic:ro
    networks:
      - web

volumes:
  shared-sessions:
  redis-data:

networks:
  web:
    driver: overlay

Key points:

  • order: start-first forces a blue‑green style rollout.

  • Healthcheck hits /health – you’ll add that route in Laravel.

  • Sessions live on a Docker volume so both old and new containers can read them.

Traefik dynamic configuration (path: traefik/dynamic/laravel.yml)

http:
  services:
    laravel:
      loadBalancer:
        serversTransport: default@internal
        healthCheck:
          path: /health
          interval: "10s"
          timeout: "2s"
        # Rolling update settings – Traefik will keep 80% of traffic on the old version until the new one passes healthcheck.
        rollingUpdate:
          maxUnavailable: 1
          maxSurge: 1
  routers:
    laravel-router:
      rule: "Host(`example.com`)"
      entryPoints:
        - web
      service: laravel
      middlewares:
        - redirect-to-https
  middlewares:
    redirect-to-https:
      redirectScheme:
        scheme: https
        permanent: true

Traefik now knows how to probe the Laravel container and shift traffic gradually – that’s the traefik rolling updates feature.

Swarm stack file (path: stack.yml)

version: "3.9"
services:
  laravel:
    image: myorg/laravel-app:latest
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 5s
        order: start-first
    networks:
      - web
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 10s
      timeout: 2s
      retries: 2
    environment:
      - APP_ENV=production
      - SESSION_DRIVER=redis
      - CACHE_DRIVER=redis
    volumes:
      - shared-sessions:/var/www/html/storage/framework/sessions

networks:
  web:
    external: true

volumes:
  shared-sessions:
    external: true

Deploy with a single command:

docker stack deploy -c stack.yml laravel-prod

Swarm will spin up a fresh replica, wait for the healthcheck, then retire the oldest one.

Laravel specific tweaks

Health endpoint (routes/web.php)

Route::get('/health', function () {
    return response()->json(['status' => 'ok']);
});

Session driver

Set SESSION_DRIVER=redis in .env. Redis runs as a service in the same overlay network, so latency stays low.

Cache warm‑up

Add a post‑deploy hook that runs php artisan config:cache && php artisan route:cache && php artisan view:cache. You can embed it in the stack file using a one‑off service:

  warmup:
    image: myorg/laravel-app:latest
    command: ["php", "artisan", "optimize"]
    depends_on:
      - laravel
    networks:
      - web
    deploy:
      restart_policy:
        condition: none

The warm‑up runs after the new containers are healthy, so the first real request hits a ready cache.

Deploy workflow

  1. Build and push the image.

docker build -t myorg/laravel-app:$(git rev-parse --short HEAD) .
docker push myorg/laravel-app:$(git rev-parse --short HEAD)
  • Update the stack to use the new tag.

    docker stack deploy -c stack.yml laravel-prod
  • Watch Traefik dashboard (http://<host>:8080/dashboard/).

  • Verify health endpoint:

    curl -I http://example.com/health

    You should see 200 OK from the newest container.

  • Run a quick load test (e.g., hey -n 1000 -c 20 http://example.com). No spikes in latency mean the rollout succeeded.

  • Mermaid diagram – request flow during rolling update

    flowchart LR
        Client -->|HTTPS| Traefik[Traefik]
        Traefik -->|Round‑Robin| ServiceA[Laravel v1]
        Traefik -->|Health‑Check| ServiceB[Laravel v2]
        ServiceA -->|Redis| Redis
        ServiceB -->|Redis| Redis
        classDef healthy fill:#9f6,stroke:#333,stroke-width:2px;
        class ServiceB healthy;

    The diagram shows how Traefik keeps sending traffic to the healthy version while the new one passes its healthcheck.

    Comparison table – rolling update vs blue‑green

    Feature

    Traefik rolling updates

    Manual blue‑green (two stacks)

    Traffic shift granularity

    Per‑container, automatic

    100 % switch via DNS or load‑balancer

    Rollback speed

    Instant – stop new containers

    Requires redeploy of the old stack

    Complexity

    Low – defined in dynamic.yml

    Higher – two separate services

    Session sharing

    Shared volume works for both

    Need external session store (Redis)

    Common Pitfalls & Troubleshooting

    • Healthcheck returns 502 – Most often the /health route is protected by auth middleware. Move it above the middleware group or add Route::middleware('web')->get('/health', …).

    • Sessions disappear after a rollout – Ensure the shared-sessions volume is declared as external: true in both compose and stack files. A missing external flag creates a new empty volume each deploy.

    • Traefik still routes to the old container after update – Check the order: start-first flag. If omitted, Swarm stops the old replica before the new one is ready, causing a brief outage.

    • Redis connection refused – In Swarm, services must be on the same overlay network. Verify networks: - web appears on both laravel and redis services.

    • Cache warm‑up never runs – The one‑off warmup service needs depends_on and restart_policy: condition: none. Without it, Swarm treats the container as a long‑running service and restarts it on failure.

    FAQ

    • How does Traefik know when a Laravel container is ready? Traefik polls the /health endpoint defined in the service’s healthCheck block. A 200 response marks the container as healthy and eligible for traffic.

    • Can I use Docker Compose without Swarm for zero downtime? Yes, by running docker compose up -d --scale app=2 and using Traefik’s start-first strategy, but Swarm gives you built‑in rolling‑update orchestration.

    • What if I need zero‑downtime database migrations? Run migrations in a separate one‑off service after the new containers are healthy. Example: docker run --rm myorg/laravel-app php artisan migrate --force.

    • Is the shared session volume safe for production? It works for small to medium traffic. For large scale, switch entirely to Redis (or Memcached) and drop the volume.


    Connect with KoderSolution šŸš€

    Master modern software engineering, scalable architectures, and advanced backend systems with KoderSolution:

    • 🌐 Official Website: Read more architectural deep-dives and tutorials on kodersolution.com

    • šŸ“ŗ YouTube Channel: Subscribe to @kodersolution on YouTube for concise, animated system design breakdowns and shorts.

    • šŸ“± Facebook Page: Follow our official Facebook Page for daily development tips, architecture discussions, and community updates.

    šŸ’¬ What's your setup? How does your team handle session persistence when doing zero‑downtime Laravel deployments on Docker, and have you run into any edge‑cases with Traefik’s rolling updates? Share your thoughts and join the discussion below!

    Related Engineering Guides

    Maksudur Rahman
    M
    Written by•Software Engineer

    Maksudur Rahman

    Senior Developer Advocate & System Lead Moderator.

    React to this article

    Recommended Articles

    View all articles