Laravel Mastery: Deployment and DevOps

Deploying Laravel without downtime: environments, Forge and Envoy, CI/CD pipelines, health checks and rollbacks, from a platform that couldn't go offline.

Updated 5 min read

A Laravel deploy should run the tests, build each release in its own folder, switch over in one step, and keep the previous release ready to roll back to. This guide covers environments, zero-downtime releases, Forge and Envoy, CI pipelines and health checks.

Environments and configuration

Use .env for secrets and settings that change between environments. Never commit it to Git. Each server keeps its own .env, or you commit an encrypted copy made with php artisan env:encrypt and decrypt it during the deploy.

In config/*.php, read the environment:

'api_key' => env('API_KEY'),

Everywhere else, read config('services.example.api_key'), never env(). Once config is cached, env() returns null outside the config files.

Build the caches on the server as part of each deploy:

php artisan optimize

Zero-downtime deployment

A zero-downtime deploy builds each release in its own folder, then switches a symlink once the release is ready. Laravel Envoyer does this, and so does a custom script:

/var/www/app/current -> /var/www/app/releases/20250626_123456

The web server points at current/public. Switching the symlink is atomic, so visitors get either the old release or the new one, never half of each. Rolling back means pointing current at the previous release.

Maintenance mode is for the deploys that can’t be zero-downtime, such as a migration that rewrites a large table. Use --secret so you can still check the site:

php artisan down --secret="super-secret-code"

Then deploy, then:

php artisan up

Deploy with Laravel Forge

  • Provision the server
  • Connect the repository on GitHub or Bitbucket
  • Configure the deploy script
  • Add environment variables
  • Run queue workers, and Horizon if you use Redis queues

Custom deployment with Envoy

composer require laravel/envoy --dev

Create Envoy.blade.php:

@servers(['prod' => 'user@yourserver'])

@task('deploy', ['on' => 'prod'])
    cd /var/www/app
    git pull origin main
    composer install --no-dev --optimize-autoloader --no-interaction
    php artisan migrate --force
    php artisan optimize
    php artisan queue:restart
@endtask

This simple version updates the code in place, so requests during the deploy can hit a half-updated app. Use the release folder approach above when that matters.

Run:

vendor/bin/envoy run deploy

CI/CD pipelines

Use GitHub Actions, GitLab CI or Bitbucket Pipelines to run the tests, and only deploy when they pass. The server builds its own caches. Anything cached on the CI runner is thrown away when the job ends.

Example .github/workflows/deploy.yml:

name: Test and deploy

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.3'
    - run: composer install --no-interaction --prefer-dist
    - run: cp .env.example .env && php artisan key:generate
    - run: php artisan test
    - name: Trigger the Forge deploy
      run: curl -fsS -X POST "${{ secrets.FORGE_DEPLOY_WEBHOOK }}"

Keep the Forge deploy URL in a repository secret, because anyone with it can trigger a deploy.

Health checks and rollbacks

  • Laravel 11 and later include a /up health route. Point an uptime monitor such as Oh Dear or Pingdom at it
  • Keep previous releases ready for rollback
  • Use Supervisor to restart failed queue workers
  • Make migrations backwards compatible, so the previous release still works if you roll back

A real example

On a Laravel trading platform, releases were built around queue restarts and backwards-compatible migrations, because the exchange couldn’t simply be taken offline for a deploy.

The checklist

  • Automate what’s repeatable
  • Run the tests before every deploy
  • Document every .env variable in .env.example
  • Build caches on the server with php artisan optimize
  • Check the health route after each deploy

If your releases still need a quiet hour and a maintenance page, this is the setup I’d put in place as your Laravel developer.

Published , updated .

Part 6 of Laravel Mastery.

Related posts