Zero Downtime Deploy Laravel Docker with Traefik
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 -Iand 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/hostsfor 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: overlayKey points:
order: start-firstforces 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: trueTraefik 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: trueDeploy with a single command:
docker stack deploy -c stack.yml laravel-prodSwarm 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: noneThe warmāup runs after the new containers are healthy, so the first real request hits a ready cache.
Deploy workflow
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-prodWatch 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 | 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
/healthroute is protected by auth middleware. Move it above the middleware group or addRoute::middleware('web')->get('/health', ā¦).Sessions disappear after a rollout ā Ensure the
shared-sessionsvolume is declared asexternal: truein 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-firstflag. 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: - webappears on bothlaravelandredisservices.Cache warmāup never runs ā The oneāoff
warmupservice needsdepends_onandrestart_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
/healthendpoint defined in the serviceāshealthCheckblock. A200response 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=2and using Traefikāsstart-firststrategy, 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!