Développement Web

Laravel déploiement production : guide pratique 2026

27 min de lecture

Faire tourner Laravel en local est facile. Le faire tourner en production fiable, performant, monitoré, sauvegardé et déployable sans downtime demande des choix précis et de la discipline opérationnelle. Ce guide rassemble les pratiques opérationnelles éprouvées pour des PME qui veulent livrer du Laravel sérieux en 2026 — versions cibles : Laravel 12.x (bug fixes jusqu’au 13 août 2026, security jusqu’au 24 février 2027) ou Laravel 13.x (sorti le 17 mars 2026, requiert PHP 8.3 à 8.5).

Voir aussi → Laravel pour PME : guide backend PHP moderne.


Sommaire

  1. Choix d’hébergement
  2. Laravel Forge : le standard managé
  3. VPS auto-hébergé
  4. Docker et conteneurisation
  5. Configuration production
  6. Queue workers en production
  7. Scheduling et cron
  8. Déploiement zéro downtime + CI/CD
  9. Monitoring et logs
  10. Sauvegardes et reprise
  11. FAQ

1. Choix d’hébergement

Six options selon contexte PME.

Mutualisé : démarrage à très bas coût, mais limites importantes (pas de queue worker dédié, pas de Redis, scheduling parfois bidouillé). Acceptable pour petites apps simples, à éviter pour du sérieux.

VPS auto-géré (Hetzner, OVH, Scaleway, hébergeurs ouest-africains) : contrôle total, prix maîtrisé, gestion technique à assumer. Compétence sysadmin nécessaire ou prestataire.

Laravel Forge (forge.laravel.com) : service officiel Laravel pour gérer des serveurs VPS. Provisioning automatique, déploiement Git, SSL automatique. Excellent rapport simplicité/prix.

PaaS (Railway, Render, Fly.io) : déploiement git push, scaling automatique. Plus cher au scale, parfait pour MVPs ou projets sans besoin de personnalisation.

Laravel Cloud (cloud.laravel.com) : plateforme managée 100 % Laravel lancée avec la version 12. Déploiement depuis un dépôt GitHub en moins d’une minute, scaling automatique, queues/Reverb/Pulse intégrés. Bonne option si on accepte un vendor lock-in et qu’on veut zéro DevOps.

Laravel Vapor : serverless AWS Lambda. Performance, mais infrastructure complexe et coûts variables.

Recommandation par profil

  • MVP / startup : Forge sur Hetzner, ou Railway/Render, ou Laravel Cloud
  • PME établie : Forge sur Hetzner / OVH, ou VPS auto-géré
  • Croissance avec ambition : Forge multi-serveurs, Laravel Cloud, ou Vapor selon stratégie

2. Laravel Forge : le standard managé

Forge est devenu le standard pour beaucoup d’équipes Laravel. Il automatise ce qui prend des heures sur un VPS vierge.

Ce que Forge fait

  • Provisioning du VPS (Nginx, PHP-FPM, MySQL/PostgreSQL, Redis, Node)
  • Configuration sécurité (firewall, fail2ban, SSH par clé)
  • Déploiement Git (push to deploy)
  • HTTPS automatique avec Let’s Encrypt
  • Queue workers managés via Supervisor
  • Scheduling via cron Forge
  • Backups automatisés vers S3/spaces
  • Monitoring serveur basique
  • Multiple sites par serveur

Workflow type

  1. Créer compte Forge, connecter provider cloud (DigitalOcean, AWS, Linode, Hetzner, Vultr, etc.)
  2. Provisionner un nouveau serveur en quelques minutes
  3. Créer un site sur le serveur en pointant un dépôt Git
  4. Configurer les variables d’environnement
  5. Déployer

Le script ci-dessous est celui que Forge génère par défaut et exécute à chaque déploiement. Comprendre chaque ligne aide à le personnaliser : on récupère la dernière version du code, on installe les dépendances en mode production (sans require-dev), on applique les migrations sans prompt interactif, on précompile les fichiers de configuration pour réduire le temps de boot, puis on signale aux workers de se recharger.

# Script de déploiement type généré par Forge
cd /home/forge/exemple.com
git pull origin main
$FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader
$FORGE_PHP artisan migrate --force
$FORGE_PHP artisan config:cache
$FORGE_PHP artisan route:cache
$FORGE_PHP artisan view:cache
$FORGE_PHP artisan event:cache
$FORGE_PHP artisan queue:restart

Output attendu : aucune erreur, chaque commande affiche un message court (Configuration cached successfully, Routes cached successfully, etc.). Signal de réussite : la page d’accueil répond en 200 et le temps de réponse mesuré ne change pas anormalement après le déploiement. Piège fréquent : sans --force, migrate demande une confirmation interactive et bloque le déploiement.

Quick deployments

Forge propose le « Quick Deploy » : chaque push sur la branche choisie déclenche un déploiement automatique. Combiné à des tests CI passants, ça simplifie énormément le workflow.

Limites

  • Pas de support pour scénarios très complexes (sharding, multi-region, microservices)
  • Forge gère un serveur à la fois ; pour cluster, plus de configuration manuelle

3. VPS auto-hébergé

Pour qui veut le contrôle total ou éviter le coût Forge.

Stack type

Une stack Laravel production-ready empile six couches. Chaque flèche du schéma marque une frontière de processus avec ses propres logs, ses propres timeouts et ses propres configurations à durcir séparément.

Internet
   ↓
Caddy ou Nginx (HTTPS Let's Encrypt)
   ↓
PHP-FPM 8.3+ (ou FrankenPHP en worker mode)
   ↓
Laravel app
   ↓
MySQL/PostgreSQL + Redis

En 2026, deux choix de moteur sont défendables : PHP-FPM classique (mature, bien documenté, OPcache + JIT) ou FrankenPHP qui combine serveur web et runtime PHP en un seul binaire Go, avec un worker mode qui garde l’application Laravel en mémoire (gains 2-4× sur le temps de réponse). Pour démarrer, restez sur PHP-FPM ; passez à FrankenPHP quand vous avez besoin du throughput supplémentaire.

Installation Ubuntu 24.04 LTS

Sur un Ubuntu 24.04 fraîchement provisionné, voici la séquence minimale pour avoir une machine prête à servir Laravel. Adaptez php8.3 en php8.4 (active support jusqu’en novembre 2026) ou php8.5 si vous voulez les dernières features (property hooks, opérateur pipe, OPcache toujours actif). Les extensions listées sont celles requises par Laravel + les drivers DB courants ; imagick est optionnel si vous ne manipulez pas d’images.

# Mises à jour
sudo apt update && sudo apt upgrade -y

# PHP 8.3 + extensions Laravel (remplacer 8.3 par 8.4 ou 8.5 si souhaité)
sudo apt install php8.3 php8.3-fpm php8.3-cli php8.3-mbstring php8.3-xml \
    php8.3-mysql php8.3-pgsql php8.3-curl php8.3-zip php8.3-bcmath \
    php8.3-redis php8.3-intl php8.3-gd php8.3-imagick

# Composer 2.x
curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer

# Nginx
sudo apt install nginx

# MySQL 8 ou PostgreSQL 16
sudo apt install mysql-server   # ou postgresql

# Redis 7
sudo apt install redis-server

# Node 22 LTS (pour Vite, Inertia, Reverb client)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install nodejs

Vérification : php -v doit afficher PHP 8.3.x (ou 8.4.x/8.5.x), composer --version doit afficher Composer version 2.x, node -v doit afficher v22.x. Piège fréquent : Ubuntu 24.04 inclut PHP 8.3 par défaut, mais pour 8.4 ou 8.5 il faut ajouter le PPA ppa:ondrej/php (sudo add-apt-repository ppa:ondrej/php) avant apt install.

Configuration Nginx

Le serveur Nginx fait deux choses : terminer le TLS et router les requêtes PHP vers PHP-FPM via une socket Unix (plus rapide que TCP localhost). La directive try_files implémente le pattern Laravel : servir les fichiers statiques s’ils existent, sinon laisser index.php gérer la route.

server {
    listen 443 ssl http2;
    server_name app.exemple.com;
    root /var/www/app/public;
    index index.php;

    ssl_certificate /etc/letsencrypt/live/app.exemple.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.exemple.com/privkey.pem;

    # Headers de sécurité utiles en prod
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

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

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Bloquer l'accès aux fichiers sensibles
    location ~ /\.(?!well-known).* { deny all; }
}

Test de configuration : sudo nginx -t doit retourner syntax is ok et test is successful. Rechargement à chaud sans couper les connexions actives : sudo systemctl reload nginx. Signal de réussite : curl -I https://app.exemple.com renvoie HTTP/2 200 et l’en-tête strict-transport-security.

Sécurité

Quelques commandes essentielles pour durcir le serveur avant d’ouvrir au monde. ufw est le firewall par défaut sur Ubuntu, simple à utiliser. fail2ban surveille les logs SSH et banne automatiquement les IPs qui font trop de tentatives infructueuses.

# Firewall UFW : autoriser SSH, HTTP, HTTPS uniquement
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

# fail2ban : protection brute-force SSH
sudo apt install fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

À ajouter : SSH par clé uniquement (désactiver PasswordAuthentication yes dans /etc/ssh/sshd_config), mises à jour de sécurité automatiques (unattended-upgrades), voir Linux hardening production pour la procédure complète.


4. Docker et conteneurisation

Pour des déploiements modernes ou scaling Kubernetes/Swarm.

Le Dockerfile suivant est un multi-stage build typique pour Laravel : un stage Composer pour les dépendances PHP, un stage Node pour bundler les assets Vite, et un stage final léger basé sur php:8.3-fpm-alpine. Le multi-stage permet d’avoir une image finale de ~150-200 Mo au lieu de 800+ Mo, et n’embarque pas Composer ni Node en production (réduction de surface d’attaque).

# Multi-stage build
FROM composer:2 AS composer
WORKDIR /app
COPY composer.* ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist

FROM node:22-alpine AS node
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM php:8.3-fpm-alpine
WORKDIR /var/www
RUN apk add --no-cache \
    libzip-dev libpng-dev nginx supervisor \
    && docker-php-ext-install pdo_mysql zip bcmath gd opcache

COPY --from=composer /app/vendor /var/www/vendor
COPY . /var/www
COPY --from=node /app/public/build /var/www/public/build

RUN composer dump-autoload --optimize \
    && php artisan config:cache \
    && php artisan route:cache \
    && php artisan view:cache

COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/

EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

Build : docker build -t ma-app:latest . doit terminer en 2-5 minutes selon le cache. Test local : docker run -p 8080:80 --env-file .env ma-app:latest puis curl http://localhost:8080 doit répondre. Piège fréquent : config:cache au build fige les variables d’env du build dans le cache — il faut soit purger ce cache au démarrage du container (entrypoint), soit ne pas mettre config:cache dans le Dockerfile. Voir Docker en production pour PME pour les principes généraux.

Compose pour orchestration

Pour un déploiement Docker simple (un seul serveur), Compose suffit. On déclare quatre services : l’app web, la DB, Redis, et un worker dédié pour les queues. Les healthcheck permettent à Compose d’attendre que la DB soit vraiment prête avant de démarrer l’app (sinon les premières requêtes plantent avec connection refused).

services:
  app:
    image: ghcr.io/ma-pme/app:latest
    restart: unless-stopped
    environment:
      APP_ENV: production
      DB_HOST: db
    depends_on:
      db: { condition: service_healthy }
      redis: { condition: service_started }
    ports:
      - "80:80"

  db:
    image: mysql:8
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: app
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root
    volumes:
      - db-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis-data:/data

  queue:
    image: ghcr.io/ma-pme/app:latest
    restart: unless-stopped
    command: php artisan queue:work --tries=3 --max-time=3600
    depends_on:
      - app

volumes:
  db-data:
  redis-data:

Démarrage : docker compose up -d doit afficher Started pour les 4 services. Surveillance : docker compose logs -f app pour suivre les requêtes en temps réel ; docker compose ps doit montrer healthy pour la DB. Mise à jour : docker compose pull && docker compose up -d (Compose redémarre les services dont l’image a changé, sans toucher aux volumes).


5. Configuration production

.env production

Le fichier .env production diffère du fichier local sur quatre points critiques : APP_DEBUG=false (sinon les stack traces fuiteront), LOG_LEVEL à warning ou error (évite de saturer les logs), des drivers cache/session/queue branchés sur Redis (et non file ou database), et un DSN Sentry pour capturer les exceptions non gérées.

APP_ENV=production
APP_DEBUG=false
APP_URL=https://app.exemple.com

LOG_CHANNEL=stack
LOG_LEVEL=warning

DB_CONNECTION=mysql
DB_HOST=...
DB_DATABASE=...

CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
BROADCAST_CONNECTION=reverb

MAIL_MAILER=smtp
SENTRY_LARAVEL_DSN=https://...

Vérification : php artisan about (Laravel 11+) affiche l’environnement, le driver de cache, la connexion DB, le driver mail, etc. Piège fréquent : laisser APP_DEBUG=true en prod expose les credentials DB dans la page d’erreur Whoops. Vérifier après chaque déploiement : curl https://app.exemple.com/route-inexistante doit retourner une page 404 sobre, pas un dump Whoops.

Optimisations obligatoires en prod

Ces commandes pré-calculent une fois pour toutes les structures qui sont reconstruites à chaque requête en mode dev : configuration agrégée, table de routage, vues compilées Blade, listeners d’événements. Sur une app moyenne, le boot Laravel passe de ~80 ms à ~15 ms.

composer install --no-dev --optimize-autoloader
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

Signal de réussite : storage/framework/cache/ et bootstrap/cache/ contiennent les fichiers compilés (config.php, routes-v7.php, events.php). Piège fréquent : config:cache casse env() appelé hors des fichiers de config — toujours lire les variables d’environnement via config('app.foo') dans le code applicatif.

OPcache

OPcache stocke en mémoire la version compilée des scripts PHP, évitant la recompilation à chaque requête. Avec validate_timestamps=0, PHP ne vérifie même plus si les fichiers ont changé — gain ~20-30 % de CPU, mais oblige à recharger OPcache à chaque déploiement. À noter : en PHP 8.5, OPcache est toujours activé (l’extension est désormais intégrée au binaire et ne peut plus être désactivée).

; php.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0   ; en prod, recharger explicitement après deploy
opcache.preload=/var/www/preload.php   ; PHP 7.4+, pré-charge code au boot
opcache.preload_user=www-data

Vérification : php -i | grep opcache.enable doit retourner opcache.enable => On => On. Reload après deploy : sudo systemctl reload php8.3-fpm (graceful, n’interrompt pas les requêtes en cours), ou cachetool opcache:reset via socket FPM si on ne veut pas reload FPM.

JIT (PHP 8.x)

Le JIT compile à chaud les bouts de code « hot » en assembleur natif. Pour des apps web standard (CRUD, requêtes DB), le gain est modeste (5-15 %) car le temps est dominé par les I/O. Pour du calcul intensif (parsing, géométrie, image processing), le gain peut atteindre 2-3×.

opcache.jit_buffer_size=100M
opcache.jit=tracing

Vérification : php -r 'var_dump(opcache_get_status()["jit"]);' affiche buffer_size: 104857600 et on: true. Recommandation : ne pas activer le JIT sans benchmarker votre app spécifique — sur certaines apps Laravel avec beaucoup de framework boilerplate, le JIT peut être neutre voire légèrement négatif.


6. Queue workers en production

Lancer un worker

Un queue worker est un processus PHP qui consomme les jobs depuis Redis (ou DB). --tries=3 autorise 3 tentatives par job avant de le marquer failed ; --timeout=90 tue le worker si un job dépasse 90 secondes (évite les jobs zombies).

php artisan queue:work --tries=3 --timeout=90

Mais pas en CLI directement : il doit être un service supervisé (Supervisor, systemd, ou Horizon) qui le redémarre automatiquement s’il crash.

Avec Supervisor (Linux)

Supervisor gère la vie des workers : démarrage automatique au boot du serveur, restart en cas de crash, parallélisation via numprocs. Le fichier de config va dans /etc/supervisor/conf.d/laravel-worker.conf.

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/log/laravel-worker.log
stopwaitsecs=3600

Recharger Supervisor après modification du fichier de config :

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
sudo supervisorctl status laravel-worker:*

Output attendu : laravel-worker:laravel-worker_00 RUNNING pid 1234, uptime 0:00:05 pour chaque worker (00 à 03 si numprocs=4). Piège fréquent : stopwaitsecs=3600 permet aux jobs en cours de finir avant le SIGKILL — sans cette ligne, Supervisor tue les workers après 10 s par défaut et certains jobs (envoi de mails, exports volumineux) sont coupés au milieu.

Avec systemd

Alternative à Supervisor, native sur Ubuntu/Debian moderne. Plus intégrée à journalctl pour les logs. Voir systemd services tutoriel pour la config détaillée.

Restart après déploiement

Quand le code change, les workers déjà en mémoire continuent à exécuter l’ancien code. queue:restart envoie un signal soft : chaque worker termine son job courant puis se redémarre (avec le nouveau code).

php artisan queue:restart

Signal de réussite : les workers réapparaissent dans supervisorctl status avec un uptime qui reset à 0. Important : queue:restart nécessite --max-time ou --max-jobs sur le worker pour fonctionner (sans, le worker boucle indéfiniment sans vérifier le signal restart).

Horizon pour Redis

laravel/horizon fournit une UI pour superviser les queues Redis. Très précieux dès qu’il y a plusieurs queues, plusieurs types de jobs, ou besoin de visualiser le throughput. Installation :

composer require laravel/horizon
php artisan horizon:install
php artisan horizon

Horizon remplace les workers Supervisor et expose un dashboard sur /horizon (à protéger par middleware en prod). On configure les workers dans config/horizon.php avec auto-scaling : démarrer avec 2 workers, monter à 10 si la queue dépasse 100 jobs en attente.

Octane + FrankenPHP : worker mode

Pour pousser plus loin que les queue workers, Laravel Octane garde l’application Laravel en mémoire entre les requêtes HTTP (au lieu de la booter à chaque requête). FrankenPHP est devenu le serveur recommandé pour Octane (alternatives : Swoole, RoadRunner).

composer require laravel/octane
php artisan octane:install --server=frankenphp
php artisan octane:start --workers=4 --max-requests=1000

Gain mesuré : sur une app Laravel moyenne, le temps de réponse passe de ~50 ms à ~15 ms. Piège critique : tout état partagé entre requêtes (variables statiques, singletons stateful, leaks de mémoire dans des packages tiers) devient un bug en production. --max-requests=1000 redémarre les workers tous les 1000 requêtes pour limiter l’impact des fuites. Avant de passer en Octane, tester en staging plusieurs jours.


7. Scheduling et cron

Laravel a son propre scheduler interne. On déclare une seule entrée cron qui appelle schedule:run toutes les minutes ; Laravel calcule en interne quels jobs doivent tourner.

* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

Vérification : php artisan schedule:list affiche toutes les tâches planifiées avec leur prochaine exécution. Test en local : php artisan schedule:work simule un appel toutes les minutes en foreground (utile pour debug).

Laravel exécute toutes les tâches définies dans app/Console/Kernel.php (Laravel 10 et antérieur) ou routes/console.php (Laravel 11+).

$schedule->command('clients:relancer')->dailyAt('09:00')->withoutOverlapping();
$schedule->job(new GenererRapportMensuel)->monthlyOn(1, '02:00');
$schedule->call(fn () => Cache::flush())->everyFifteenMinutes();
$schedule->command('backup:run')->dailyAt('03:00')->onOneServer();

Lecture du code : la première ligne lance une commande Artisan chaque jour à 9 h en empêchant le chevauchement (utile si une exécution prend plus de 24 h). La deuxième dispatch un job sur la queue le 1er de chaque mois à 2 h. La quatrième est cluster-aware : sur un déploiement multi-serveurs, seul un serveur exécutera la tâche (sinon le backup tournerait 3 fois).

Bonnes pratiques

  • withoutOverlapping() : empêche deux exécutions parallèles de la même tâche
  • onOneServer() : utile sur cluster, ne lance la tâche que sur un serveur (nécessite cache Redis/Memcached)
  • runInBackground() : pour des tâches longues qui ne doivent pas bloquer le scheduler
  • emailOutputTo('admin@...') : envoie le résultat par email
  • onFailure(fn() => ...) : déclenche un callback si la commande retourne un code non-zéro

8. Déploiement zéro downtime + CI/CD

Stratégie sans framework dédié

Forge fait du « zero downtime » via symlinks atomiques. Le principe : on prépare la nouvelle version dans un dossier séparé, on bascule un lien symbolique vers ce dossier en une opération atomique du système de fichiers, et on recharge PHP-FPM. Aucun moment où Nginx ne trouve pas le code à servir.

  1. Cloner le code dans un nouveau dossier releases/timestamp/
  2. Installer dépendances, lancer migrations
  3. Switcher le symlink current vers le nouveau dossier
  4. Recharger PHP-FPM
# Pseudo-code
TIMESTAMP=$(date +%s)
RELEASE_DIR=/var/www/releases/$TIMESTAMP

# 1. Cloner la nouvelle version
git clone --depth 1 -b main https://github.com/ma-pme/app.git $RELEASE_DIR
cd $RELEASE_DIR

# 2. Installer dépendances en mode production
composer install --no-dev --optimize-autoloader

# 3. Lier le storage et le .env partagés (persistance des uploads entre releases)
ln -sfn /var/www/shared/.env .env
ln -sfn /var/www/shared/storage storage

# 4. Caches Laravel
php artisan config:cache route:cache view:cache event:cache

# 5. Migrations (forcer pour éviter le prompt)
php artisan migrate --force

# 6. Symlink atomique : remplace /var/www/current en une seule syscall
ln -sfn $RELEASE_DIR /var/www/current

# 7. Reload PHP-FPM (graceful) + relancer les workers
sudo systemctl reload php8.3-fpm
php artisan queue:restart

# 8. Garder uniquement les 5 dernières releases (rollback rapide)
cd /var/www/releases && ls -t | tail -n +6 | xargs rm -rf

Rollback en cas de pépin : ln -sfn /var/www/releases/<previous_timestamp> /var/www/current && sudo systemctl reload php8.3-fpm. C’est instantané et c’est tout l’intérêt de garder les 5 dernières releases.

Pipeline CI/CD GitHub Actions

Pour un déploiement zero-downtime depuis GitHub, on combine un workflow CI (tests, analyse statique, build d’image Docker) à un workflow CD (SSH vers le serveur, exécution du script ci-dessus). Voici un exemple complet pour un déploiement Forge-like, déclenché sur push main.

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          coverage: none
      - uses: actions/setup-node@v4
        with: { node-version: '22' }
      - run: composer install --no-progress --prefer-dist
      - run: npm ci && npm run build
      - run: cp .env.testing .env
      - run: php artisan key:generate
      - run: php artisan migrate --force
      - run: vendor/bin/pest --parallel

  deploy:
    needs: test
    runs-on: ubuntu-24.04
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /var/www/app
            ./deploy.sh

Signal de réussite : l’étape deploy passe en vert sur GitHub, et un curl -I https://app.exemple.com retourne un nouveau header X-App-Version (à injecter dans le script de déploiement). Piège fréquent : sans needs: test, un déploiement peut partir avec des tests rouges. Sécurité : la clé SSH stockée dans secrets.PROD_SSH_KEY doit être une clé dédiée au déploiement, avec accès restreint à cd /var/www/app && ./deploy.sh via command= dans ~/.ssh/authorized_keys.

Outils de déploiement

  • Forge : intégré, recommandé pour PME
  • Envoyer (envoyer.io) : déploiement zero-downtime made by Laravel, UI propre
  • Deployer (deployer.org) : open-source, équivalent Capistrano, scriptable en PHP
  • Laravel Cloud : automatique sur push GitHub, géré par Laravel
  • Custom : avec scripts bash + GitHub Actions (exemple ci-dessus)

Migrations production

Migrations bloquantes (ALTER sur grosse table, ajout NOT NULL, drop colonne utilisée) doivent être décomposées en plusieurs déploiements pour éviter le lock de table qui bloque les requêtes :

  1. Déploiement A : ajouter colonne nullable
  2. Backfill via job en arrière-plan (par batch de 10 000 lignes)
  3. Déploiement B : code utilise nouvelle colonne, ancienne ignorée
  4. Migration : ajouter contrainte NOT NULL (rapide car colonne déjà remplie)
  5. Déploiement C : drop ancienne colonne

Sur PostgreSQL, ajouter CREATE INDEX CONCURRENTLY au lieu de CREATE INDEX évite le lock de table pendant l’indexation. Sur MySQL, ALTER TABLE ... ALGORITHM=INPLACE, LOCK=NONE quand c’est supporté. Outil intéressant : gh-ost (GitHub Online Schema Tool) pour les très grosses tables.


9. Monitoring et logs

Sentry pour les erreurs

Sentry capture chaque exception non gérée avec sa stack trace complète, le contexte utilisateur, les breadcrumbs (les actions qui ont précédé l’erreur), et l’environnement (release, OS, navigateur côté front). Le tier gratuit autorise 5 000 erreurs/mois — suffisant pour démarrer une PME.

composer require sentry/sentry-laravel
php artisan sentry:publish --dsn=https://...

Le sentry:publish ajoute le DSN dans .env et publie la config par défaut. Vérification : php artisan sentry:test envoie une exception factice ; elle apparaît dans le dashboard Sentry en quelques secondes. Conseil : configurer le sample rate des performances à 0.1 (10 % des requêtes tracées) pour rester dans le tier gratuit avec un trafic modéré.

Logs centralisés

Sur un seul serveur, lire les logs avec tail -f storage/logs/laravel.log suffit. Dès qu’il y a plus d’un serveur, il faut centraliser : Loki (avec Promtail), ELK (Elasticsearch-Logstash-Kibana), ou des services managés (Datadog, Better Stack / ex-Logtail, Papertrail, Axiom).

// config/logging.php : channel pour Better Stack (ex-Logtail)
'logtail' => [
    'driver' => 'monolog',
    'handler' => Logtail\Monolog\LogtailHandler::class,
    'with' => ['sourceToken' => env('LOGTAIL_SOURCE_TOKEN')],
],

Activer en mettant LOG_CHANNEL=logtail dans .env, ou en ajoutant logtail au stack channel pour cumuler local + distant. Bonne pratique : structurer les logs en JSON pour faciliter la recherche ('formatter' => \Monolog\Formatter\JsonFormatter::class).

Laravel Pulse

Pulse est le dashboard officiel Laravel pour monitorer en temps réel les requêtes lentes, les exceptions, l’usage des queues, le cache hit rate, et la santé des serveurs. Gratuit, intégré nativement, faible overhead (~1 % CPU).

composer require laravel/pulse
php artisan vendor:publish --provider="Laravel\Pulse\PulseServiceProvider"
php artisan migrate

Dashboard sur /pulse (à protéger via le middleware Gate::define('viewPulse', ...) dans app/Providers/AuthServiceProvider.php). Métriques utiles : requêtes lentes (>1 s), exceptions par minute, queue depth, cache hit rate, usage Redis. Pour Reverb, Pulse expose en plus le nombre de connexions WebSocket actives — utile dès qu’on broadcast en temps réel.

Métriques Prometheus / Grafana

Pour aller plus loin : spatie/laravel-prometheus expose un endpoint /metrics au format Prometheus. Couplé à Grafana, dashboards persistants avec alertes (Slack, PagerDuty, email).

Health checks robustes

Endpoint /health appelé par le load balancer et le monitoring externe (UptimeRobot, Better Stack Uptime, Pingdom). Doit vérifier que les dépendances critiques répondent — pas juste que PHP tourne.

Route::get('/health', function () {
    $checks = [];

    // DB : ping
    try {
        DB::connection()->getPdo();
        $checks['db'] = 'ok';
    } catch (\Throwable $e) {
        $checks['db'] = 'fail';
    }

    // Redis : ping
    try {
        Redis::ping();
        $checks['redis'] = 'ok';
    } catch (\Throwable $e) {
        $checks['redis'] = 'fail';
    }

    // Queue : taille
    $checks['queue_size'] = Queue::size();

    // Disque : pourcentage libre
    $free = disk_free_space('/') / disk_total_space('/');
    $checks['disk_free_pct'] = round($free * 100, 1);

    $healthy = $checks['db'] === 'ok' && $checks['redis'] === 'ok' && $free > 0.10;

    return response()->json(
        ['status' => $healthy ? 'ok' : 'degraded', 'checks' => $checks, 'time' => now()],
        $healthy ? 200 : 503
    );
});

Signal de réussite : curl https://app.exemple.com/health renvoie un JSON avec status: ok et un code 200. Si la DB tombe : code 503, ce qui dit au load balancer de retirer ce serveur du pool. Piège fréquent : ne pas mettre cet endpoint derrière l’auth — sinon le monitoring externe ne peut pas le joindre.


10. Sauvegardes et reprise

Spatie Laravel Backup

Le package spatie/laravel-backup automatise les backups : dump de la base de données + archive des fichiers (storage, uploads), upload vers S3/Backblaze B2/FTP, rotation configurable, notifications en cas d’échec via Slack/email.

composer require spatie/laravel-backup
php artisan vendor:publish --provider="Spatie\Backup\BackupServiceProvider"
php artisan backup:run --only-db   # test

Configurer dans config/backup.php : chemins à backuper (source.files.include), destinations (destination.disks), notifications (notifications.mail.to), rotation (cleanup.default_strategy.keep_daily_backups_for_days).

// Schedule (dans routes/console.php pour Laravel 11+)
$schedule->command('backup:clean')->daily()->at('01:00');
$schedule->command('backup:run')->daily()->at('01:30');
$schedule->command('backup:monitor')->daily()->at('02:00');  // alerte si pas de backup récent

Output attendu : Backup created on disk s3 / backups/exemple/2026-05-26-013015.zip (218 MiB). Signal de réussite : 7 jours après l’install, vérifier que le bucket S3 contient bien 7 archives quotidiennes. Piège fréquent : si l’app envoie des notifications Slack en cas d’échec mais que le DSN Slack lui-même est cassé, on ne sait jamais que le backup a échoué — toujours doubler avec backup:monitor qui alerte si aucun backup récent.

Tester les restaurations

Un backup non testé n’est pas un backup. Au moins une fois par trimestre :

  • Restaurer le backup le plus récent dans un environnement isolé (Docker local ou VPS jetable)
  • Lancer les migrations et vérifier que l’app boot
  • Vérifier l’intégrité des données (compter les lignes des tables critiques, vérifier les FK)
  • Documenter le processus de restauration (runbook) avec les commandes exactes
  • Chronométrer la durée totale — c’est le RTO (Recovery Time Objective) réel de l’organisation

Voir Sauvegarde 3-2-1 PME africaine pour la stratégie générale applicable à toute application Laravel en production.


11. FAQ

Forge ou VPS auto-géré ?

Forge si l’équipe veut se concentrer sur le code et accepte un coût mensuel raisonnable (12 $/serveur). VPS auto-géré si compétence sysadmin présente et volonté de contrôle total. Pour la majorité des PME : Forge sur Hetzner offre le meilleur compromis.

Comment déployer sans interruption ?

Forge ou Envoyer pour faire ça en 2 clics. En self-hosted : symlinks atomiques + reload PHP-FPM + queue:restart. Voir section dédiée. Avec FrankenPHP en worker mode, le reload se fait via SIGUSR2 sans interruption.

Octane vaut-il la peine ?

Pour des applications avec trafic significatif et besoins de performance : oui. Pour des back-offices internes ou trafic modéré : pas indispensable. Vérifier la compatibilité avec ses dépendances avant de se lancer (state qui fuite entre requêtes = bugs en prod). En 2026, FrankenPHP est devenu le serveur Octane recommandé.

Logs en production : que conserver ?

Logs applicatifs au niveau warning+ (level=warning), erreurs dans Sentry, logs d’accès web (Nginx). Conservation : 30-90 jours selon volume et obligations légales.

Comment mesurer la performance Laravel en prod ?

Laravel Pulse (officiel, gratuit), Sentry Performance, New Relic, Datadog APM. Profiling occasionnel avec Telescope en preprod. Métriques clés : p95/p99 par route, taux d’erreur, queue throughput, cache hit rate.

Combien de queue workers ?

Démarrer avec 2-4 workers. Augmenter si la queue s’accumule. Surveiller via Horizon ou logs queue. Pour des apps avec beaucoup de jobs : queues nommées par priorité (default, emails, exports) et workers dédiés par priorité.

Migrations bloquantes : comment les éviter ?

Décomposer en étapes compatibles : ajout colonne nullable → backfill par batch → contrainte NOT NULL. Sur PostgreSQL, utiliser CREATE INDEX CONCURRENTLY. Sur MySQL, ALTER TABLE ... ALGORITHM=INPLACE, LOCK=NONE ou gh-ost. Tester les migrations sur copie de la prod avant le go-live.

Laravel 12 ou Laravel 13 en production ?

Laravel 13 (sorti le 17 mars 2026) est la version actuelle, requiert PHP 8.3 minimum. Laravel 12 reçoit encore des bug fixes jusqu’au 13 août 2026 et des security fixes jusqu’au 24 février 2027 — parfait pour les applications déjà en prod qui n’ont pas besoin de migrer immédiatement. Pour un nouveau projet en mai 2026 : démarrer directement en Laravel 13.


Articles liés (cluster Laravel)

Partager