Saltar al contenido

Laravel Envoy es la herramienta oficial de Laravel para automatizar tareas en servidores remotos. Si estás cansado de conectarte por SSH para hacer deploys manuales, ejecutar migraciones o reiniciar colas, Envoy te permite hacerlo todo con un solo comando desde tu terminal local.

En esta guía completa aprenderás desde la instalación básica hasta estrategias avanzadas de deploy con zero-downtime, rollback automático y notificaciones a tu equipo.

¿Qué es Laravel Envoy y por qué usarlo?

Envoy es un task runner que utiliza sintaxis Blade para definir comandos que se ejecutan en servidores remotos vía SSH. A diferencia de otras herramientas de automatización, Envoy está diseñado específicamente para desarrolladores Laravel y ofrece una curva de aprendizaje mínima.

Características principales

Laravel Envoy ofrece un conjunto de funcionalidades que lo hacen ideal para equipos de desarrollo Laravel:

La sintaxis Blade permite definir tareas usando el mismo sistema de templates que ya conoces de Laravel. No necesitas aprender un nuevo DSL ni configuraciones complejas en YAML.

El soporte para múltiples servidores te permite ejecutar comandos en varios servidores simultáneamente, ya sea de forma secuencial o en paralelo.

Las Stories agrupan tareas relacionadas para ejecutarlas en secuencia con un solo comando, ideal para flujos de deploy complejos.

Los Hooks (@before, @after, @error, @success, @finished) permiten ejecutar código PHP antes y después de las tareas, perfecto para validaciones y limpieza.

Las notificaciones integradas envían alertas a Slack, Discord, Telegram o Microsoft Teams cuando las tareas terminan.

Envoy vs otras alternativas

Antes de elegir Envoy, es importante entender cómo se compara con otras soluciones:

Envoy vs Deploy manual por SSH: El deploy manual es propenso a errores humanos. Olvidar un comando, ejecutar en el orden incorrecto o en el servidor equivocado puede causar downtime. Envoy garantiza consistencia en cada deploy.

Envoy vs GitHub Actions / GitLab CI: Las pipelines de CI/CD son potentes pero requieren configuración compleja. Envoy es ideal para proyectos pequeños y medianos donde necesitas control inmediato desde tu máquina local sin esperar a que termine una pipeline.

Envoy vs Ansible: Ansible es una herramienta de configuración de infraestructura completa. Si solo necesitas automatizar deploys y tareas de mantenimiento en servidores ya configurados, Envoy es mucho más simple y directo.

Envoy vs Deployer: Deployer (deployer.org) es similar a Envoy pero con sintaxis PHP pura. Envoy tiene la ventaja de usar Blade y estar mantenido oficialmente por el equipo de Laravel.

Envoy vs Laravel Forge / Envoyer: Forge y Envoyer son servicios de pago que ofrecen UI web, zero-downtime deploys y más. Envoy es gratuito y te da control total desde la terminal, ideal si prefieres no depender de servicios externos.

Cuándo usar Laravel Envoy

Envoy es la elección correcta cuando:

  • Trabajas con proyectos Laravel de cualquier tamaño

  • Tu equipo es pequeño o mediano y busca simplicidad

  • Necesitas deploys frecuentes con un flujo consistente

  • Prefieres control directo desde tu terminal

  • No quieres pagar por servicios de deploy

  • Ya tienes servidores configurados y solo necesitas automatizar tareas

Considera otras alternativas si:

  • Necesitas pipelines complejos con múltiples stages y aprobaciones

  • Requieres integración profunda con herramientas de testing en cada commit

  • Tu equipo ya domina otras soluciones de DevOps como Ansible o Kubernetes

  • Necesitas gestión de infraestructura, no solo deploys

Requisitos previos

Antes de empezar con Envoy, asegúrate de tener:

Sistema operativo compatible: Envoy funciona en Mac y Linux de forma nativa. En Windows puedes usarlo a través de WSL2 (Windows Subsystem for Linux).

Acceso SSH configurado: Necesitas poder conectarte a tus servidores sin contraseña usando llaves SSH. Si al ejecutar ssh user@tu-servidor te pide contraseña, primero configura el acceso con llaves.

PHP instalado localmente: Envoy se ejecuta desde tu máquina local, así que necesitas PHP instalado (el mismo que usas para desarrollo).

Proyecto Laravel: Aunque Envoy puede usarse en cualquier proyecto PHP, está diseñado para Laravel y aprovecha mejor sus capacidades en este contexto.

Configurar acceso SSH sin contraseña

Si aún no tienes configurado el acceso SSH con llaves, estos son los pasos:

# Generar llave SSH si no tienes una
ssh-keygen -t ed25519 -C "[email protected]"

# Copiar la llave pública al servidor
ssh-copy-id user@tu-servidor-ip

# Verificar que funciona sin pedir contraseña
ssh user@tu-servidor-ip

Si usas múltiples llaves SSH para diferentes servidores, puedes configurarlas en ~/.ssh/config:

Host produccion
    HostName 192.168.1.100
    User deploy
    IdentityFile ~/.ssh/id_produccion

Host staging
    HostName 192.168.1.101
    User deploy
    IdentityFile ~/.ssh/id_staging

Con esta configuración, puedes usar los alias directamente en Envoy.

Instalación de Laravel Envoy

La instalación de Envoy es sencilla. Desde el directorio raíz de tu proyecto Laravel:

composer require laravel/envoy --dev

El flag --dev es importante porque Envoy solo se necesita en desarrollo, no en producción.

Una vez instalado, el binario está disponible en vendor/bin/envoy. Puedes verificar la instalación ejecutando:

php vendor/bin/envoy --version

Crear el archivo Envoy.blade.php

Todas las tareas de Envoy se definen en un archivo llamado Envoy.blade.php en la raíz de tu proyecto:

touch Envoy.blade.php

Este archivo usa sintaxis Blade pero no es un template HTML, sino un script de tareas.

Anatomía de Envoy.blade.php

El archivo Envoy.blade.php tiene una estructura específica con directivas que definen servidores, configuración, tareas y hooks.

@servers: Definir servidores

La directiva @servers define los servidores donde se ejecutarán las tareas. Debe estar en una sola línea:

@servers(['web' => '[email protected]', 'worker' => '[email protected]'])

Puedes definir múltiples servidores y agruparlos:

@servers([
    'web-1' => '[email protected]',
    'web-2' => '[email protected]',
    'worker' => '[email protected]',
    'localhost' => '127.0.0.1'
])

El servidor localhost con IP 127.0.0.1 ejecuta comandos en tu máquina local, útil para tareas de preparación.

@setup: Configuración inicial

La directiva @setup permite definir variables PHP que estarán disponibles en todas las tareas:

@setup
    $repository = '[email protected]:tu-usuario/tu-proyecto.git';
    $appDir = '/var/www/tu-proyecto';
    $branch = $branch ?? 'main';
    $releaseDir = $appDir . '/releases/' . date('YmdHis');
    $currentDir = $appDir . '/current';
@endsetup

Aquí puedes usar cualquier código PHP: variables, condicionales, funciones, etc.

@include: Cargar archivos PHP

Si necesitas usar clases o funciones externas, puedes incluir el autoloader de Composer:

@include('vendor/autoload.php')

@setup
    $config = json_decode(file_get_contents('deploy-config.json'), true);
@endsetup

@import: Importar otros archivos Envoy

Puedes modularizar tus tareas importando otros archivos Envoy:

@import('envoy/tasks.blade.php')
@import('envoy/notifications.blade.php')

@task: Definir tareas

Las tareas son el núcleo de Envoy. Cada tarea define comandos shell que se ejecutan en los servidores especificados:

@task('deploy', ['on' => 'web'])
    cd {{ $appDir }}
    git pull origin {{ $branch }}
    composer install --no-dev --optimize-autoloader
    php artisan migrate --force
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
@endtask

Las opciones disponibles para @task son:

  • on: Servidor o array de servidores donde ejecutar

  • parallel: Ejecutar en paralelo en múltiples servidores (default: false)

  • confirm: Pedir confirmación antes de ejecutar (default: false)

@story: Agrupar tareas

Las stories permiten ejecutar múltiples tareas en secuencia con un solo comando:

@story('deploy')
    git-pull
    composer-install
    migrate
    cache-clear
    optimize
@endstory

@task('git-pull', ['on' => 'web'])
    cd {{ $appDir }}
    git pull origin {{ $branch }}
@endtask

@task('composer-install', ['on' => 'web'])
    cd {{ $appDir }}
    composer install --no-dev --optimize-autoloader
@endtask

@task('migrate', ['on' => 'web'])
    cd {{ $appDir }}
    php artisan migrate --force
@endtask

@task('cache-clear', ['on' => 'web'])
    cd {{ $appDir }}
    php artisan optimize:clear
@endtask

@task('optimize', ['on' => 'web'])
    cd {{ $appDir }}
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
@endtask

Las stories mejoran la organización y permiten ejecutar tareas individuales cuando lo necesites.

Hooks: @before, @after, @error, @success, @finished

Los hooks ejecutan código PHP en momentos específicos del ciclo de vida de las tareas:

@before
    if ($task === 'deploy') {
        // Validaciones antes del deploy
    }
@endbefore

@after
    if ($task === 'deploy') {
        // Limpieza después del deploy
    }
@endafter

@error
    // Se ejecuta si una tarea falla
    // $task contiene el nombre de la tarea que falló
@enderror

@success
    // Se ejecuta si TODAS las tareas terminan sin errores
@endsuccess

@finished
    // Se ejecuta siempre, haya o no errores
    // $exitCode contiene el código de salida (0 = éxito)
    if ($exitCode > 0) {
        // Hubo errores
    }
@endfinished

Los hooks se ejecutan localmente, no en los servidores remotos.

Ejecutar tareas

Para ejecutar una tarea o story:

php vendor/bin/envoy run deploy

Pasar variables

Puedes pasar variables desde la línea de comandos:

php vendor/bin/envoy run deploy --branch=feature/nueva-funcionalidad

Y usarlas en tus tareas:

@task('deploy', ['on' => 'web'])
    cd {{ $appDir }}
    git pull origin {{ $branch }}
@endtask

Confirmar antes de ejecutar

Para tareas peligrosas, añade la opción confirm:

@task('migrate-fresh', ['on' => 'web', 'confirm' => true])
    cd {{ $appDir }}
    php artisan migrate:fresh --seed
@endtask

Envoy pedirá confirmación antes de ejecutar.

Ejemplos prácticos

Deploy básico

El ejemplo más simple de deploy:

@servers(['web' => 'deploy@tu-servidor'])

@setup
    $appDir = '/var/www/tu-proyecto';
    $branch = $branch ?? 'main';
@endsetup

@task('deploy', ['on' => 'web'])
    echo "Iniciando deploy..."
    
    cd {{ $appDir }}
    
    echo "Activando modo mantenimiento..."
    php artisan down --retry=60
    
    echo "Actualizando código..."
    git pull origin {{ $branch }}
    
    echo "Instalando dependencias..."
    composer install --no-dev --optimize-autoloader
    
    echo "Ejecutando migraciones..."
    php artisan migrate --force
    
    echo "Limpiando y optimizando caché..."
    php artisan optimize:clear
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
    
    echo "Desactivando modo mantenimiento..."
    php artisan up
    
    echo "Deploy completado!"
@endtask

Ejecutar:

php vendor/bin/envoy run deploy

Deploy en múltiples servidores

Para aplicaciones con varios servidores web:

@servers([
    'web-1' => '[email protected]',
    'web-2' => '[email protected]',
    'web-3' => '[email protected]'
])

@setup
    $appDir = '/var/www/tu-proyecto';
    $branch = $branch ?? 'main';
@endsetup

@task('deploy', ['on' => ['web-1', 'web-2', 'web-3'], 'parallel' => true])
    cd {{ $appDir }}
    git pull origin {{ $branch }}
    composer install --no-dev --optimize-autoloader
    php artisan optimize:clear
    php artisan config:cache
@endtask

Con parallel => true, el deploy se ejecuta simultáneamente en los tres servidores.

Deploy con múltiples entornos

Para manejar desarrollo, staging y producción:

@servers([
    'dev' => '[email protected]',
    'staging' => '[email protected]',
    'production' => '[email protected]'
])

@setup
    $appDir = '/var/www/tu-proyecto';
    
    // Mapeo de entorno a rama
    $branches = [
        'dev' => 'develop',
        'staging' => 'staging',
        'production' => 'main'
    ];
    
    // Validar que se especificó un entorno
    if (!isset($env)) {
        throw new Exception('Debes especificar el entorno: --env=dev|staging|production');
    }
    
    if (!isset($branches[$env])) {
        throw new Exception("Entorno '$env' no válido");
    }
    
    $branch = $branches[$env];
@endsetup

@task('deploy', ['on' => $env, 'confirm' => $env === 'production'])
    echo "Desplegando en {{ $env }} (rama: {{ $branch }})..."
    
    cd {{ $appDir }}
    
    @if($env === 'production')
        php artisan down --retry=60
    @endif
    
    git pull origin {{ $branch }}
    composer install --no-dev --optimize-autoloader
    php artisan migrate --force
    php artisan optimize:clear
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
    
    @if($env === 'production')
        php artisan up
    @endif
    
    echo "Deploy en {{ $env }} completado!"
@endtask

Ejecutar:

# Deploy en desarrollo
php vendor/bin/envoy run deploy --env=dev

# Deploy en staging
php vendor/bin/envoy run deploy --env=staging

# Deploy en producción (pedirá confirmación)
php vendor/bin/envoy run deploy --env=production

Reiniciar workers y colas

Tarea específica para reiniciar procesos de cola:

@servers([
    'worker-1' => '[email protected]',
    'worker-2' => '[email protected]'
])

@setup
    $appDir = '/var/www/tu-proyecto';
@endsetup

@task('restart-queues', ['on' => ['worker-1', 'worker-2'], 'parallel' => true])
    cd {{ $appDir }}
    php artisan queue:restart
    echo "Colas reiniciadas en este servidor"
@endtask

@task('restart-horizon', ['on' => ['worker-1', 'worker-2'], 'parallel' => true])
    cd {{ $appDir }}
    php artisan horizon:terminate
    echo "Horizon reiniciado en este servidor"
@endtask

Ejecutar comandos Artisan remotamente

A veces necesitas ejecutar comandos específicos:

@task('tinker', ['on' => $env ?? 'production'])
    cd {{ $appDir }}
    php artisan tinker
@endtask

@task('logs', ['on' => $env ?? 'production'])
    cd {{ $appDir }}
    tail -f storage/logs/laravel.log
@endtask

@task('cache-clear', ['on' => $env ?? 'production'])
    cd {{ $appDir }}
    php artisan optimize:clear
    echo "Caché limpiada"
@endtask

Backup de base de datos

Automatizar backups antes del deploy:

@setup
    $appDir = '/var/www/tu-proyecto';
    $backupDir = '/var/backups/mysql';
    $timestamp = date('Y-m-d_H-i-s');
@endsetup

@task('backup-db', ['on' => 'web'])
    echo "Creando backup de base de datos..."
    
    cd {{ $appDir }}
    
    # Obtener credenciales del .env
    DB_DATABASE=$(grep DB_DATABASE .env | cut -d '=' -f2)
    DB_USERNAME=$(grep DB_USERNAME .env | cut -d '=' -f2)
    DB_PASSWORD=$(grep DB_PASSWORD .env | cut -d '=' -f2)
    
    # Crear directorio si no existe
    mkdir -p {{ $backupDir }}
    
    # Crear backup
    mysqldump -u$DB_USERNAME -p$DB_PASSWORD $DB_DATABASE > {{ $backupDir }}/backup_{{ $timestamp }}.sql
    
    # Comprimir
    gzip {{ $backupDir }}/backup_{{ $timestamp }}.sql
    
    echo "Backup creado: {{ $backupDir }}/backup_{{ $timestamp }}.sql.gz"
    
    # Eliminar backups de más de 7 días
    find {{ $backupDir }} -name "*.sql.gz" -mtime +7 -delete
    
    echo "Backups antiguos eliminados"
@endtask

@story('deploy-with-backup')
    backup-db
    deploy
@endstory

Deploy Zero-Downtime

El deploy con zero-downtime usa enlaces simbólicos para cambiar de versión instantáneamente. La estructura de directorios es:

/var/www/tu-proyecto/
├── current -> releases/20241206120000  (symlink)
├── releases/
│   ├── 20241206120000/
│   ├── 20241206100000/
│   └── 20241206080000/
└── shared/
    ├── .env
    └── storage/

Implementación completa:

@servers(['web' => 'deploy@tu-servidor'])

@setup
    $repository = '[email protected]:tu-usuario/tu-proyecto.git';
    $baseDir = '/var/www/tu-proyecto';
    $releasesDir = $baseDir . '/releases';
    $sharedDir = $baseDir . '/shared';
    $currentDir = $baseDir . '/current';
    
    $branch = $branch ?? 'main';
    $release = date('YmdHis');
    $releaseDir = $releasesDir . '/' . $release;
    
    // Número de releases a mantener
    $keepReleases = 5;
@endsetup

@story('deploy')
    prepare
    clone
    composer
    shared
    migrate
    optimize
    activate
    cleanup
@endstory

@task('prepare', ['on' => 'web'])
    echo "Preparando directorios..."
    
    # Crear estructura de directorios si no existe
    mkdir -p {{ $releasesDir }}
    mkdir -p {{ $sharedDir }}/storage/app/public
    mkdir -p {{ $sharedDir }}/storage/framework/cache
    mkdir -p {{ $sharedDir }}/storage/framework/sessions
    mkdir -p {{ $sharedDir }}/storage/framework/views
    mkdir -p {{ $sharedDir }}/storage/logs
    
    # Verificar que existe .env en shared
    if [ ! -f {{ $sharedDir }}/.env ]; then
        echo "ERROR: No existe {{ $sharedDir }}/.env"
        echo "Crea el archivo .env en {{ $sharedDir }} antes de continuar"
        exit 1
    fi
@endtask

@task('clone', ['on' => 'web'])
    echo "Clonando repositorio (release: {{ $release }})..."
    
    git clone --depth 1 --branch {{ $branch }} {{ $repository }} {{ $releaseDir }}
    
    echo "Código clonado correctamente"
@endtask

@task('composer', ['on' => 'web'])
    echo "Instalando dependencias..."
    
    cd {{ $releaseDir }}
    composer install --no-dev --optimize-autoloader --no-interaction
@endtask

@task('shared', ['on' => 'web'])
    echo "Enlazando archivos compartidos..."
    
    cd {{ $releaseDir }}
    
    # Eliminar storage y enlazar al compartido
    rm -rf storage
    ln -s {{ $sharedDir }}/storage storage
    
    # Enlazar .env
    ln -s {{ $sharedDir }}/.env .env
    
    echo "Enlaces simbólicos creados"
@endtask

@task('migrate', ['on' => 'web'])
    echo "Ejecutando migraciones..."
    
    cd {{ $releaseDir }}
    php artisan migrate --force
@endtask

@task('optimize', ['on' => 'web'])
    echo "Optimizando aplicación..."
    
    cd {{ $releaseDir }}
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
    php artisan storage:link
@endtask

@task('activate', ['on' => 'web'])
    echo "Activando nueva versión..."
    
    # Cambiar el enlace current a la nueva release
    ln -sfn {{ $releaseDir }} {{ $currentDir }}
    
    # Reiniciar PHP-FPM para limpiar opcache
    sudo systemctl reload php8.3-fpm
    
    echo "Nueva versión activada: {{ $release }}"
@endtask

@task('cleanup', ['on' => 'web'])
    echo "Limpiando releases antiguas..."
    
    cd {{ $releasesDir }}
    
    # Mantener solo las últimas N releases
    ls -dt */ | tail -n +{{ $keepReleases + 1 }} | xargs -r rm -rf
    
    echo "Limpieza completada. Releases actuales:"
    ls -la {{ $releasesDir }}
@endtask

Rollback

Si algo sale mal, puedes volver a la versión anterior:

@task('rollback', ['on' => 'web', 'confirm' => true])
    echo "Iniciando rollback..."
    
    cd {{ $releasesDir }}
    
    # Obtener la release anterior
    PREVIOUS=$(ls -dt */ | sed -n '2p' | tr -d '/')
    
    if [ -z "$PREVIOUS" ]; then
        echo "ERROR: No hay release anterior para rollback"
        exit 1
    fi
    
    echo "Volviendo a release: $PREVIOUS"
    
    # Cambiar el enlace
    ln -sfn {{ $releasesDir }}/$PREVIOUS {{ $currentDir }}
    
    # Reiniciar PHP-FPM
    sudo systemctl reload php8.3-fpm
    
    echo "Rollback completado a: $PREVIOUS"
@endtask

@task('list-releases', ['on' => 'web'])
    echo "Releases disponibles:"
    ls -la {{ $releasesDir }}
    echo ""
    echo "Release actual:"
    readlink {{ $currentDir }}
@endtask

Notificaciones

Envoy puede enviar notificaciones cuando las tareas terminan.

Slack

@setup
    $slackWebhook = 'https://hooks.slack.com/services/XXX/YYY/ZZZ';
@endsetup

@finished
    @slack($slackWebhook, '#deploys', "Deploy completado en {$env}")
@endfinished

Con mensaje personalizado según el resultado:

@finished
    @if($exitCode === 0)
        @slack($slackWebhook, '#deploys', "✅ Deploy exitoso en producción")
    @else
        @slack($slackWebhook, '#deploys', "❌ Error en deploy de producción")
    @endif
@endfinished

Discord

@setup
    $discordWebhook = 'https://discord.com/api/webhooks/XXX/YYY';
@endsetup

@finished
    @discord($discordWebhook, "Deploy completado con código: {$exitCode}")
@endfinished

Telegram

@setup
    $telegramBotId = 'tu-bot-id';
    $telegramChatId = 'tu-chat-id';
@endsetup

@finished
    @telegram($telegramBotId, $telegramChatId)
@endfinished

Microsoft Teams

@setup
    $teamsWebhook = 'https://outlook.office.com/webhook/XXX';
@endsetup

@finished
    @microsoftTeams($teamsWebhook)
@endfinished

Buenas prácticas

Seguridad

Usa un usuario específico para deploys con permisos limitados. No uses root nunca.

# En el servidor, crear usuario deploy
sudo adduser deploy
sudo usermod -aG www-data deploy

# Dar permisos de reinicio a PHP-FPM sin contraseña
echo "deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload php8.3-fpm" | sudo tee /etc/sudoers.d/deploy

Estructura de directorios

Mantén los archivos sensibles fuera del repositorio:

/var/www/proyecto/
├── current/              # Symlink a release actual
├── releases/             # Versiones del código
├── shared/               # Archivos persistentes
│   ├── .env
│   └── storage/
└── backups/              # Backups de BD

Evitar errores comunes

No ejecutes composer con root: Composer muestra warnings y puede causar problemas de permisos.

No olvides --force en migraciones: En producción, Laravel pide confirmación para migraciones. Usa --force para automatizar.

Limpia el opcache: Después de actualizar código, PHP puede seguir sirviendo código cacheado. Reinicia PHP-FPM o limpia opcache.

Verifica antes de desplegar: Usa un hook @before para verificar que el servidor está accesible:

@before
    if ($task === 'deploy') {
        exec("ssh -o ConnectTimeout=5 deploy@tu-servidor 'echo ok' 2>/dev/null", $output, $code);
        if ($code !== 0) {
            throw new Exception("No se puede conectar al servidor");
        }
    }
@endbefore

Testing local de tareas

Puedes probar tareas en localhost antes de ejecutar en producción:

@servers(['localhost' => '127.0.0.1'])

@task('test', ['on' => 'localhost'])
    echo "Probando tarea..."
    pwd
    ls -la
@endtask

Solución de problemas

Error: Permission denied

Verifica que tu llave SSH tiene acceso al servidor:

ssh -v deploy@tu-servidor

Error: Host key verification failed

Añade el servidor a known_hosts:

ssh-keyscan -H tu-servidor >> ~/.ssh/known_hosts

Las tareas no muestran output

Envoy muestra el output en tiempo real. Si no ves nada, verifica que los comandos producen salida con echo.

El deploy funciona pero la web no actualiza

PHP puede estar cacheando el código. Reinicia PHP-FPM:

sudo systemctl reload php8.3-fpm

O limpia opcache desde PHP:

@task('clear-opcache', ['on' => 'web'])
    cd {{ $appDir }}
    php -r "opcache_reset();"
@endtask

Ejemplo completo de Envoy.blade.php

Este es un ejemplo completo listo para usar en producción:

@servers([
    'production' => '[email protected]',
    'staging' => '[email protected]'
])

@setup
    $repository = '[email protected]:tu-usuario/tu-proyecto.git';
    $baseDir = '/var/www/tu-proyecto';
    $releasesDir = $baseDir . '/releases';
    $sharedDir = $baseDir . '/shared';
    $currentDir = $baseDir . '/current';
    
    $branch = $branch ?? 'main';
    $release = date('YmdHis');
    $releaseDir = $releasesDir . '/' . $release;
    $keepReleases = 5;
    
    $slackWebhook = 'https://hooks.slack.com/services/XXX/YYY/ZZZ';
    
    // Determinar servidor
    $server = $on ?? 'staging';
    if (!in_array($server, ['production', 'staging'])) {
        throw new Exception("Servidor no válido: $server");
    }
@endsetup

@story('deploy', ['on' => $server])
    clone-release
    install-dependencies
    link-shared
    run-migrations
    optimize-app
    activate-release
    cleanup-old
@endstory

@task('clone-release')
    echo "📦 Clonando {{ $branch }} en {{ $releaseDir }}..."
    git clone --depth 1 --branch {{ $branch }} {{ $repository }} {{ $releaseDir }}
@endtask

@task('install-dependencies')
    echo "📥 Instalando dependencias..."
    cd {{ $releaseDir }}
    composer install --no-dev --optimize-autoloader --no-interaction
@endtask

@task('link-shared')
    echo "🔗 Enlazando archivos compartidos..."
    cd {{ $releaseDir }}
    rm -rf storage
    ln -s {{ $sharedDir }}/storage storage
    ln -s {{ $sharedDir }}/.env .env
@endtask

@task('run-migrations')
    echo "🗄️ Ejecutando migraciones..."
    cd {{ $releaseDir }}
    php artisan migrate --force
@endtask

@task('optimize-app')
    echo "⚡ Optimizando aplicación..."
    cd {{ $releaseDir }}
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
    php artisan storage:link
@endtask

@task('activate-release')
    echo "🚀 Activando release {{ $release }}..."
    ln -sfn {{ $releaseDir }} {{ $currentDir }}
    sudo systemctl reload php8.3-fpm
@endtask

@task('cleanup-old')
    echo "🧹 Limpiando releases antiguas..."
    cd {{ $releasesDir }}
    ls -dt */ | tail -n +{{ $keepReleases + 1 }} | xargs -r rm -rf
@endtask

@task('rollback', ['on' => $server, 'confirm' => true])
    echo "⏪ Iniciando rollback..."
    cd {{ $releasesDir }}
    PREVIOUS=$(ls -dt */ | sed -n '2p' | tr -d '/')
    
    if [ -z "$PREVIOUS" ]; then
        echo "❌ No hay release anterior"
        exit 1
    fi
    
    ln -sfn {{ $releasesDir }}/$PREVIOUS {{ $currentDir }}
    sudo systemctl reload php8.3-fpm
    echo "✅ Rollback a $PREVIOUS completado"
@endtask

@task('restart-queues', ['on' => $server])
    echo "🔄 Reiniciando colas..."
    cd {{ $currentDir }}
    php artisan queue:restart
@endtask

@task('logs', ['on' => $server])
    cd {{ $currentDir }}
    tail -f storage/logs/laravel.log
@endtask

@success
    @slack($slackWebhook, '#deploys', "✅ Deploy exitoso en {$server}")
@endsuccess

@error
    @slack($slackWebhook, '#deploys', "❌ Error en deploy de {$server}")
@enderror

Uso:

# Deploy en staging
php vendor/bin/envoy run deploy --on=staging

# Deploy en producción
php vendor/bin/envoy run deploy --on=production

# Rollback
php vendor/bin/envoy run rollback --on=production

# Ver logs
php vendor/bin/envoy run logs --on=production

Conclusión

Laravel Envoy es la herramienta perfecta para equipos Laravel que buscan automatizar sus deploys sin la complejidad de soluciones enterprise. Con una sintaxis familiar y capacidades potentes como ejecución paralela, hooks y notificaciones, puedes crear flujos de deploy profesionales en minutos.

La estrategia de zero-downtime con releases y symlinks te permite desplegar con confianza, sabiendo que puedes hacer rollback instantáneamente si algo sale mal.

Si quieres profundizar más en deploys automatizados con Envoy, incluyendo configuración completa de servidores Ubuntu, SSL con Let's Encrypt y deploy en EC2, puedes consultar nuestro curso completo de Laravel Envoy.

Laravel Envoy representa la filosofía Laravel: herramientas potentes con sintaxis elegante que mejoran tu productividad sin añadir complejidad innecesaria.

school Curso completo

Curso Laravel 12
Completo 2026

El único curso 100% actualizado que incluye Laravel 12, Livewire 3, Vue 3, React 19 e Inertia 2. Aprende con proyectos reales y las últimas funcionalidades.

access_time 8 horas de contenido
layers 4 tecnologías en 1
update 100% actualizado
code Proyectos prácticos
Ver Curso Laravel 12 arrow_forward

star Incluido en cualquier suscripción

Rutas de aprendizaje