DevOps

laravel-deploy

Configures deployment for Forge, Vapor, Docker, Bref

Overview

The laravel-deploy agent configures production deployment. It sets up zero-downtime deployments, creates Docker configurations, generates deployment scripts for various platforms, and configures health checks and monitoring.

Responsibilities

  • Zero-Downtime Deployment - Atomic deployments with symlinks
  • Platform Configuration - Forge, Vapor, Ploi, custom VPS
  • Docker Setup - Production-ready Dockerfiles
  • Health Checks - Endpoint monitoring for load balancers
  • Environment Config - Production-ready settings
  • SSL/HTTPS - Certificate configuration

What It Creates

Component File Purpose
Dockerfile Dockerfile Container image definition
Docker Compose docker-compose.yml Multi-container orchestration
Deploy Script deploy.sh Deployment automation
Health Check app/Http/Controllers/HealthController.php Load balancer health endpoint

Generated Dockerfile

# Production Dockerfile
FROM php:8.3-fpm-alpine

# Install dependencies
RUN apk add --no-cache \
    nginx \
    supervisor \
    libpng-dev \
    libzip-dev \
    && docker-php-ext-install pdo_mysql gd zip opcache

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Configure PHP
COPY docker/php.ini /usr/local/etc/php/conf.d/custom.ini

# Configure Nginx
COPY docker/nginx.conf /etc/nginx/http.d/default.conf

# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf

# Set working directory
WORKDIR /var/www/html

# Copy application
COPY --chown=www-data:www-data . .

# Install dependencies
RUN composer install --no-dev --optimize-autoloader

# Cache config
RUN php artisan config:cache && \
    php artisan route:cache && \
    php artisan view:cache

EXPOSE 80

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

Zero-Downtime Deploy Script

#!/bin/bash
set -e

DEPLOY_DIR="/var/www/app"
RELEASES_DIR="$DEPLOY_DIR/releases"
SHARED_DIR="$DEPLOY_DIR/shared"
CURRENT="$DEPLOY_DIR/current"
RELEASE=$(date +%Y%m%d%H%M%S)

echo "Starting deployment: $RELEASE"

# Clone fresh release
git clone --depth 1 git@github.com:user/repo.git "$RELEASES_DIR/$RELEASE"
cd "$RELEASES_DIR/$RELEASE"

# Link shared resources
ln -nfs "$SHARED_DIR/.env" .env
ln -nfs "$SHARED_DIR/storage" storage

# Install dependencies
composer install --no-dev --optimize-autoloader

# Build assets
npm ci && npm run build

# Cache everything
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

# Run migrations
php artisan migrate --force

# Health check before switching
if ! curl -sf http://localhost:8080/health > /dev/null; then
    echo "Health check failed, aborting deployment"
    rm -rf "$RELEASES_DIR/$RELEASE"
    exit 1
fi

# Atomic switch
ln -nfs "$RELEASES_DIR/$RELEASE" "$CURRENT"

# Restart services
php artisan queue:restart
sudo systemctl reload php-fpm

# Cleanup old releases (keep 5)
ls -dt "$RELEASES_DIR"/* | tail -n +6 | xargs rm -rf

echo "Deployment complete: $RELEASE"

Laravel Forge Configuration

# Forge deployment script
cd /home/forge/example.com

git pull origin $FORGE_SITE_BRANCH

$FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader

( flock -w 10 9 || exit 1
    echo 'Restarting FPM...'; sudo -S service $FORGE_PHP_FPM reload ) 9>/tmp/fpmlock

if [ -f artisan ]; then
    $FORGE_PHP artisan migrate --force
    $FORGE_PHP artisan config:cache
    $FORGE_PHP artisan route:cache
    $FORGE_PHP artisan view:cache
    $FORGE_PHP artisan queue:restart
fi

npm ci && npm run build

Health Check Endpoint

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;

class HealthController extends Controller
{
    public function __invoke()
    {
        $checks = [
            'status' => 'ok',
            'timestamp' => now()->toIso8601String(),
            'checks' => [
                'database' => $this->checkDatabase(),
                'cache' => $this->checkCache(),
                'redis' => $this->checkRedis(),
            ],
        ];

        $healthy = collect($checks['checks'])
            ->every(fn ($check) => $check['status'] === 'ok');

        return response()->json($checks, $healthy ? 200 : 503);
    }

    private function checkDatabase(): array
    {
        try {
            DB::select('SELECT 1');
            return ['status' => 'ok'];
        } catch (\Exception $e) {
            return ['status' => 'fail', 'error' => $e->getMessage()];
        }
    }
}

Invoked By Commands

Guardrails

The deploy agent follows strict rules:

  • ALWAYS backup before deploying
  • ALWAYS run migrate --force with --pretend first
  • ALWAYS include health checks before switching
  • ALWAYS keep at least 5 previous releases for rollback
  • NEVER deploy with APP_DEBUG=true
  • NEVER expose .env files in public

See Also