Laravel es un framework PHP que en producción necesita un servidor web, PHP-FPM, una base de datos y un par de procesos auxiliares: el worker de colas y el programador de tareas. En este tutorial desplegarás una aplicación Laravel en Ubuntu 24.04 con Nginx, PHP 8.3, MySQL 8 y un certificado de Let's Encrypt, y dejarás la cola y el programador funcionando como servicios del sistema.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 1 GB de RAM.
  • Un usuario no root con privilegios sudo. En los ejemplos se llama your_user.
  • Un dominio con un registro DNS A apuntando a la IP del servidor. En los ejemplos se usa your_domain.
  • El código de tu aplicación en un repositorio Git, o las ganas de probar con un proyecto Laravel nuevo.

Los ejemplos usan myapp como nombre de la aplicación y de la base de datos. Sustitúyelo por el tuyo en todos los comandos.

Paso 1: Instalar Nginx, PHP 8.3 y MySQL

Ubuntu 24.04 incluye PHP 8.3 en sus repositorios, que cumple los requisitos de las versiones actuales de Laravel. Instala Nginx, PHP-FPM con las extensiones que usa Laravel, MySQL, Composer y las utilidades para descomprimir paquetes:

sudo apt update
sudo apt install nginx mysql-server php8.3-fpm php8.3-cli php8.3-mysql php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip php8.3-bcmath php8.3-intl composer git unzip

Comprueba las versiones instaladas:

php -v
composer --version
PHP 8.3.6 (cli) (built: ...) (NTS)
...
Composer version 2.7.1 ...

Verifica que PHP-FPM está activo, ya que Nginx le pasará las peticiones PHP a través de su socket:

systemctl status php8.3-fpm --no-pager
● php8.3-fpm.service - The PHP 8.3 FastCGI Process Manager
     Active: active (running) since ...

Abre en el cortafuegos los puertos HTTP y HTTPS, además de SSH:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Paso 2: Crear la base de datos

En Ubuntu, el usuario root de MySQL se autentica por socket, así que puedes entrar con sudo sin contraseña:

sudo mysql

Crea la base de datos y un usuario exclusivo para la aplicación. Sustituye your_strong_password por una contraseña larga y aleatoria:

CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'myapp'@'localhost' IDENTIFIED BY 'your_strong_password';
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'localhost';
EXIT;

Comprueba que el nuevo usuario puede conectarse:

mysql -u myapp -p -e "SHOW DATABASES;"
+--------------------+
| Database           |
+--------------------+
| information_schema |
| myapp              |
| performance_schema |
+--------------------+

Paso 3: Descargar el código de la aplicación

Crea el directorio de la aplicación y haz a tu usuario propietario, con el grupo www-data que usan Nginx y PHP-FPM:

sudo mkdir -p /var/www/myapp
sudo chown your_user:www-data /var/www/myapp

Si tienes la aplicación en un repositorio, clónala e instala las dependencias sin los paquetes de desarrollo:

git clone https://github.com/your_account/myapp.git /var/www/myapp
cd /var/www/myapp
composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generate

Si solo quieres probar el despliegue, crea en su lugar un proyecto nuevo, que ya genera el archivo .env y la clave de la aplicación:

composer create-project laravel/laravel /var/www/myapp
cd /var/www/myapp

En ambos casos, comprueba que Artisan funciona y que la clave existe:

php artisan --version
grep APP_KEY .env
Laravel Framework 12.x.x
APP_KEY=base64:...

Paso 4: Configurar el entorno de producción

Abre el archivo .env:

nano /var/www/myapp/.env

Ajusta estas variables y deja el resto como están. Con APP_DEBUG=false los errores no se muestran al visitante, y los controladores database guardan sesiones, caché y cola en MySQL sin necesidad de instalar Redis:

APP_ENV=production
APP_DEBUG=false
APP_URL=https://your_domain

LOG_LEVEL=warning

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=myapp
DB_PASSWORD=your_strong_password

SESSION_DRIVER=database
CACHE_STORE=database
QUEUE_CONNECTION=database

Paso 5: Ajustar los permisos

PHP-FPM se ejecuta como www-data y necesita escribir en storage (logs, sesiones, archivos subidos) y en bootstrap/cache. El resto del código solo necesita ser legible. Asigna el grupo www-data, da permiso de escritura al grupo solo en esos dos directorios y activa el bit setgid para que los archivos nuevos hereden el grupo:

cd /var/www/myapp
sudo chgrp -R www-data storage bootstrap/cache
sudo chmod -R g+w storage bootstrap/cache
sudo find storage bootstrap/cache -type d -exec chmod g+s {} +

El archivo .env contiene la contraseña de la base de datos. Haz que solo tu usuario y el grupo www-data puedan leerlo:

chmod 640 .env
sudo chgrp www-data .env

A partir de aquí, ejecuta los comandos de Artisan que escriben archivos como www-data. Así los logs y la caché que generan siempre pertenecen al usuario del servidor web y no aparecen errores de permisos más tarde.

Paso 6: Ejecutar las migraciones y optimizar

Crea las tablas de la aplicación. En producción, Laravel pide confirmación antes de migrar; --force la omite:

sudo -u www-data php artisan migrate --force
   INFO  Running migrations.

  0001_01_01_000000_create_users_table ........................ 25.31ms DONE
  0001_01_01_000001_create_cache_table ........................ 9.12ms DONE
  0001_01_01_000002_create_jobs_table ......................... 21.40ms DONE

Crea el enlace simbólico public/storage para servir los archivos subidos y genera las cachés de configuración, rutas, eventos y vistas:

sudo -u www-data php artisan storage:link
sudo -u www-data php artisan optimize

Paso 7: Configurar Nginx

Crea un bloque de servidor para el dominio:

sudo nano /etc/nginx/sites-available/myapp

Pega esta configuración, basada en la recomendada por la documentación de Laravel. La raíz apunta a public/, de modo que .env y el resto del código nunca son accesibles desde la web:

server {
    listen 80;
    listen [::]:80;
    server_name your_domain www.your_domain;
    root /var/www/myapp/public;

    index index.php;
    charset utf-8;
    client_max_body_size 20M;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

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

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Activa el sitio, desactiva el sitio por defecto y comprueba la sintaxis:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Recarga Nginx y comprueba que la aplicación responde:

sudo systemctl reload nginx
curl -I http://your_domain
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Content-Type: text/html; charset=utf-8
...

Paso 8: Activar HTTPS con Let's Encrypt

Instala Certbot con su plugin para Nginx:

sudo apt install certbot python3-certbot-nginx

Solicita el certificado. Certbot valida el dominio, añade la configuración TLS al bloque de servidor y crea la redirección de HTTP a HTTPS:

sudo certbot --nginx -d your_domain -d www.your_domain

El paquete instala un temporizador de systemd que renueva los certificados automáticamente. Comprueba que la renovación funciona:

sudo certbot renew --dry-run

Abre https://your_domain en el navegador: deberías ver tu aplicación, o la página de bienvenida de Laravel si creaste un proyecto nuevo.

Paso 9: Ejecutar la cola de trabajos con systemd

Los trabajos enviados a la cola (correos, notificaciones, procesos largos) solo se ejecutan si hay un worker escuchando. Crea un servicio de systemd para mantenerlo siempre en marcha:

sudo nano /etc/systemd/system/myapp-queue.service
[Unit]
Description=Laravel queue worker (myapp)
After=network.target mysql.service

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/php /var/www/myapp/artisan queue:work --sleep=3 --tries=3 --max-time=3600
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

La opción --max-time=3600 hace que el worker termine cada hora y systemd lo vuelva a lanzar, lo que evita que acumule memoria. Activa e inicia el servicio:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp-queue
systemctl status myapp-queue --no-pager
● myapp-queue.service - Laravel queue worker (myapp)
     Active: active (running) since ...

Puedes seguir lo que procesa con journalctl -u myapp-queue -f.

Paso 10: Programar las tareas de Laravel

Si la aplicación define tareas programadas, Laravel necesita que schedule:run se ejecute cada minuto. Edita el crontab de www-data:

sudo crontab -u www-data -e

Añade esta línea al final:

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

Comprueba qué tareas tiene registradas la aplicación y cuándo se ejecutarán:

sudo -u www-data php artisan schedule:list

Actualizar la aplicación

Cuando publiques cambios, este es el orden habitual para desplegarlos. El modo mantenimiento muestra una página 503 mientras se aplican las migraciones:

cd /var/www/myapp
sudo -u www-data php artisan down
git pull
composer install --no-dev --optimize-autoloader
sudo -u www-data php artisan migrate --force
sudo -u www-data php artisan optimize
sudo -u www-data php artisan queue:restart
sudo -u www-data php artisan up

queue:restart indica al worker que termine tras el trabajo actual, y systemd lo arranca de nuevo con el código actualizado.

Solución de problemas

  • Error 500 sin detalle: con APP_DEBUG=false el error solo aparece en storage/logs/laravel.log. Revísalo con tail -n 50 /var/www/myapp/storage/logs/laravel.log.
  • Permission denied en storage/logs/laravel.log: el archivo lo creó tu usuario al ejecutar Artisan sin sudo -u www-data. Corrige el propietario con sudo chown -R www-data:www-data /var/www/myapp/storage/logs.
  • 502 Bad Gateway: Nginx no puede hablar con PHP-FPM. Comprueba que el servicio está activo con systemctl status php8.3-fpm y que la ruta del socket en Nginx es /run/php/php8.3-fpm.sock.
  • Los cambios en .env no se aplican: la configuración está en caché. Ejecuta sudo -u www-data php artisan optimize de nuevo.

Conclusión

Tu aplicación Laravel está en producción con Nginx, PHP-FPM 8.3 y MySQL, servida por HTTPS, con la cola de trabajos gestionada por systemd y el programador ejecutándose cada minuto. Como siguientes pasos puedes:

  • Instalar Redis y cambiar CACHE_STORE, SESSION_DRIVER y QUEUE_CONNECTION a redis si el tráfico crece.
  • Programar copias de seguridad diarias de la base de datos con mysqldump.
  • Automatizar la secuencia de actualización con una herramienta como Deployer o un pipeline de CI.