Deploy de Laravel 13 en un VPS: Nginx, PHP-FPM y cero downtime paso a paso
Tu app Laravel 13 funciona en local y en cuanto la subes a un VPS da 500, 502 o 419. El deploy clásico con Nginx y PHP-FPM no es magia: son pasos verificables desde un servidor Ubuntu vacío hasta el HTTPS con cero downtime, y esta guía de deploy de Laravel en VPS los recorre.
Qué necesitas antes del primer deploy
Requisitos de Laravel 13: PHP 8.3 mínimo y un servidor preparado
La mayoría de los fallos del primer deploy son de versiones: Laravel 13, lanzado en marzo de 2026, fija PHP 8.3 como versión mínima, las guías de 2026 recomiendan PHP 8.4 en producción, y con PHP 8.2 el deploy falla antes de arrancar. Además del intérprete necesitas las extensiones de Laravel (mbstring, xml, curl, bcmath y el driver de tu base, pdo-mysql o pgsql) y Composer 2 para instalar dependencias. Opera con un usuario sudo (nunca root) y un usuario de despliegue dueño de los archivos, y actualiza el sistema antes de instalar nada.
Tu repositorio git debe excluir el .env (contiene credenciales), vendor/ (se instala con Composer en el servidor) y node_modules (se compila en local o en CI): subirlos por error es un problema de seguridad y de coherencia.
Instalar el stack: Nginx, PHP-FPM y la base de datos
php-fpm con su socket y la base de datos
Nginx no ejecuta PHP: se lo pasa a PHP-FPM, que escucha en un socket Unix o un puerto TCP. Instala el paquete php-fpm de tu versión, comprueba que el servicio esté activo y anota el socket (por ejemplo /run/php/php8.4-fpm.sock): es el valor que usará Nginx en fastcgi_pass. Crea también una base dedicada a la app y un usuario con privilegios solo sobre esa base; evita usar el administrador de la base en el .env.
Composer install y el .env de producción
Con el código en el servidor, instala dependencias con composer install --no-dev --prefer-dist --optimize-autoloader: omites paquetes de desarrollo y generas un autoloader rápido; los assets con Vite se compilan en local o en CI. Luego configura el .env con APP_ENV=production, APP_DEBUG=false y las credenciales reales: con true, cualquier error expone stack traces con rutas y credenciales, y genera la key si el archivo no la trae.
Permisos que sí y permisos que no
storage/ y bootstrap/cache: lo único que debe ser escribible
Los permisos son el problema más frecuente en producción, y la solución rápida de los foros casi siempre es la equivocada. Laravel escribe en storage/ (logs, sesiones, caché, archivos subidos) y en bootstrap/cache: da permisos de escritura sobre esos dos directorios al usuario del servidor web o al de despliegue, y nada más. El chmod -R 777 arregla el síntoma y deja que cualquier proceso modifique tu código; los permisos correctos van al usuario adecuado, en los directorios que lo necesitan. Y si el worker corre como otro usuario, los archivos que genere deben poder leerlos el resto de procesos.
Configurar Nginx para Laravel
Document root public/, try_files y fastcgi_pass
Mal configurado, el server block te da 404 en rutas que funcionan o PHP descargándose en lugar de ejecutarse. El document root apunta a public/, nunca a la raíz del proyecto: así el servidor solo expone el front controller y el resto del código queda fuera de las peticiones. Un server block mínimo enruta con try_files toda petición que no sea un archivo real hacia el front controller:
server {
listen 80;
server_name tudominio.com;
root /var/www/app/current/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
}La línea fastcgi_pass debe apuntar al mismo socket del pool de PHP-FPM o verás 502; el include fastcgi-php.conf aporta variables estándar como SCRIPT_FILENAME, que dice a PHP qué archivo ejecutar.
Errores típicos: 502, 404 en rutas y assets rotos
El 502 es que Nginx no alcanza a PHP-FPM (socket mal escrito, servicio caído o permisos); el 404 en rutas que existen suele ser un document root mal apuntado o falta de try_files. Y si los assets cargan rotos, revisa que Nginx sirva public/build o public/storage con sus bloques location.
Migraciones y caches: encender la app
migrate --force y los caches de producción
Ejecuta php artisan migrate --force, que omite la confirmación interactiva: en el primer deploy crea las tablas y en una actualización aplica solo las pendientes. Después cachea configuración, rutas, vistas y eventos (config:cache, route:cache, view:cache, event:cache, o el combinado php artisan optimize): es lo que separa un deploy lento de uno rápido.
El error 419 post-deploy casi siempre viene de la cache de configuración: cacheaste con una key o sesión distinta de producción, o cambiaste el .env después de cachear. Regenera el .env correcto y vuelve a ejecutar config:cache. Antes de darlo por bueno, php artisan about muestra entorno, PHP y drivers en una tabla.
Colas y tareas programadas en producción
Worker como servicio systemd y queue:restart
Si procesas jobs, necesitas dos procesos que no existen en local: el worker y el scheduler. El worker corre como proceso persistente, no dentro de una petición: un servicio systemd es la vía directa en Ubuntu (define php artisan queue:work --sleep=3 --tries=3 con el usuario del proyecto y reinicio automático); Supervisor es la alternativa clásica. Como carga el código en memoria, tras cada deploy ejecuta php artisan queue:restart para que los workers se reinicien con el código nuevo; va en tu script tras el flip del symlink.
El scheduler en crontab y el driver de cola
Laravel no necesita un cron por tarea: registra todas en app/Console/Kernel y una línea de crontab ejecuta el scheduler cada minuto: * * * * * php artisan schedule:run. Esa línea apunta a la ruta del proyecto y corre con el usuario con permisos sobre storage/. El driver de cola en producción es database (solo creas la tabla de jobs) o Redis si ya está en tu stack; el driver sync ejecuta jobs en el mismo proceso y no debe usarse en producción.
HTTPS con Let's Encrypt
Certbot y proxy headers
En 2026 no hay excusa para no tener HTTPS: los certificados de Let's Encrypt son gratuitos y su renovación se automatiza. Instala Certbot, emite el certificado y deja que configure Nginx; la renovación se programa como temporizador del sistema, y un certificado caducado convierte tu web en un cartel de error. Después configura TrustProxies para confiar en Nginx como proxy: si Nginx termina el TLS y pasa la petición por HTTP, sin eso Laravel genera URLs con http; con TrustProxies usan HTTPS.
Deploy con cero downtime: el patrón de releases
releases/ + current y los archivos shared
Actualizar la app sin cortar el servicio tiene una solución estándar en un solo VPS: el patrón de releases con symlink, sin Kubernetes ni orquestación extra. Cada deploy se construye completo en una carpeta nueva (releases/2026-09-05-1015) y el servidor sirve current, un symlink al release activo: cambiar el symlink es atómico y ninguna petición ve el deploy a medias. El .env y el contenido de storage/ no se duplican en cada release: viven en una carpeta shared y el release nuevo crea symlinks hacia ellos.
Script de deploy, rollback y migraciones aditivas
El deploy repetible es un script: prepara el código del release nuevo, composer install con flags de producción, copia el .env compartido, crea los symlinks de storage, ejecuta migraciones aditivas, cachea config/rutas/vistas, reinicia el worker con queue:restart y, al final, cambia el symlink. El orden protege al release viejo si algo falla antes del flip. Y si el release nuevo falla, el rollback es volver a apuntar el symlink al anterior y reiniciar lo necesario: guarda los últimos cinco releases y el rollback tarda segundos.
Las migraciones aditivas merecen regla propia: hasta el flip, el release viejo sigue sirviendo peticiones, así que deben añadir columnas o tablas sin eliminar ni renombrar lo que el código viejo usa; una destructiva antes del flip rompe el release anterior.
Checklist post-deploy y resolución de problemas
Verificar logs, colas y scheduler, y las cinco trampas comunes
Revisa los logs de Laravel en storage/logs y los de Nginx en /var/log/nginx, comprueba que el worker esté activo y que el scheduler esté en el crontab: un deploy no termina hasta que logs, colas y scheduler lo confirman. Los fallos recurrentes del primer deploy son cinco: PHP por debajo de 8.3, el 502 por un socket de PHP-FPM mal configurado, el 419 por cache de configuración con un .env incorrecto, permisos de más (chmod 777) o de menos en storage y bootstrap/cache, y olvidar reiniciar el worker tras actualizar el código. Si tu deploy falla, revisa esa lista antes de tocar nada más.
Conclusión
Desplegar Laravel 13 en un VPS con Nginx y PHP-FPM es un proceso de pasos verificables: prepara el servidor con PHP 8.4, sube solo el código, apunta el server block a public/, cachea la configuración, levanta colas y scheduler como servicios y adopta el patrón de releases para actualizar sin cortar el servicio. Es más trabajo inicial que una plataforma gestionada, pero te deja dueño del servidor.