Qué necesitas antes de empezar
- Un VPS con Ubuntu 24.04 LTS (o Debian 13) y al menos 2 GB de RAM.
- Acceso SSH como root o como usuario con
sudo. - Un dominio con un registro A apuntando a la IP pública del servidor.
- Tu proyecto Laravel en un repositorio Git accesible desde el servidor.
Sobre las versiones: Laravel 13 se publicó el 17 de marzo de 2026 y requiere PHP 8.3 o superior, admitiendo 8.3, 8.4 y 8.5. Ubuntu 24.04 trae PHP 8.3 en sus repositorios oficiales y Debian 13 trae PHP 8.4, así que ninguna de las dos necesita repositorios externos. Si tu aplicación aún usa Laravel 11 o 12, revisa su requisito de PHP antes de tocar el servidor.
Paso 1: Actualizar el sistema
Un servidor recién aprovisionado casi siempre tiene paquetes con parches pendientes, incluidos parches de seguridad. Actualizarlo antes de instalar nada evita arrastrar vulnerabilidades conocidas desde el primer día.
sudo apt update && sudo apt upgrade -y Paso 2: Crear un usuario sin privilegios de root
Operar de forma permanente como root significa que cualquier comando equivocado, o cualquier proceso comprometido, tiene control total del servidor. Un usuario normal con sudo obliga a un paso consciente antes de cada acción privilegiada.
sudo adduser deploy
sudo usermod -aG sudo deploy Cambia a ese usuario para el resto del proceso:
su - deploy Paso 3: Deshabilitar el acceso root por SSH
Bloquear el login directo de root elimina la mitad del trabajo de un atacante: ya no basta con adivinar una contraseña, hay que adivinar además un nombre de usuario. Es el ajuste de SSH con mejor relación entre esfuerzo y protección.
sudo nano /etc/ssh/sshd_config Localiza y ajusta:
PermitRootLogin no Aplica el cambio:
sudo systemctl restart ssh Antes de cerrar tu sesión actual, abre una segunda terminal y comprueba que puedes entrar con el usuario deploy. Si algo salió mal, la sesión original sigue abierta para corregirlo. Una nota específica de Ubuntu 24.04: SSH usa activación por socket, así que si además cambias la directiva Port tendrás que ejecutar sudo systemctl daemon-reload && sudo systemctl restart ssh.socket para que el puerto nuevo tenga efecto.
Paso 4: Configurar el firewall
Permite SSH antes de activar el firewall, nunca al revés. UFW aplica por defecto la política de bloquear todo el tráfico entrante, así que activarlo sin la regla de SSH corta tu propia conexión de inmediato.
sudo ufw allow OpenSSH
sudo ufw allow 'Apache Full'
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable
sudo ufw status El perfil Apache Full abre los puertos 80 y 443 y solo existe una vez que Apache está instalado; si aún no lo has hecho, usa sudo ufw allow 80,443/tcp o vuelve a este paso después. En la guía de firewalls en Linux desarrollamos las políticas y cómo recuperarte si te bloqueas.
Paso 5: Instalar Apache
Apache sirve las peticiones HTTP y, con mod_php, ejecuta el código PHP dentro del mismo proceso. Es la opción más directa de configurar; más abajo comparamos cuándo conviene Nginx en su lugar.
sudo apt install apache2 -y
sudo systemctl enable --now apache2 Visita http://tu_ip: si aparece la página por defecto de Apache, el tráfico externo llega bien al servidor.
Paso 6: Instalar y asegurar MySQL
El asistente mysql_secure_installation elimina las configuraciones inseguras por defecto: usuarios anónimos, base de datos de prueba y acceso root remoto. Saltárselo deja abiertas puertas que no aportan nada y sí figuran en cualquier escaneo automatizado.
sudo apt install mysql-server -y
sudo mysql_secure_installation Crea una base de datos y un usuario dedicados a la aplicación. Nunca uses el usuario root de MySQL en tu .env: si la aplicación se ve comprometida, un usuario limitado a una sola base de datos contiene el daño.
sudo mysql CREATE DATABASE laravel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'tu_contraseña_segura';
GRANT ALL PRIVILEGES ON laravel_db.* TO 'laravel_user'@'localhost';
FLUSH PRIVILEGES;
EXIT; El juego de caracteres utf8mb4 no es un detalle cosmético: es el que permite almacenar emojis y caracteres de cuatro bytes. Crear la base con el utf8 antiguo provoca errores al guardar contenido que los usuarios sí escriben todos los días.
Paso 7: Instalar PHP y sus extensiones
Laravel necesita un conjunto concreto de extensiones de PHP; si falta alguna, Composer se niega a instalar las dependencias con un mensaje que no siempre es claro. Instalarlas todas de una vez evita ese ciclo de prueba y error.
sudo apt install php libapache2-mod-php php-mysql php-cli php-curl \
php-mbstring php-xml php-zip php-bcmath php-gd php-intl -y
php -v Qué hace cada una de las menos evidentes: mbstring maneja cadenas multibyte (acentos y ñ incluidos), bcmath hace aritmética de precisión arbitraria que usan varios paquetes de facturación, intl aporta formatos de fecha y número por idioma, y gd procesa imágenes.
Para producción conviene además ajustar dos valores de /etc/php/8.3/apache2/php.ini:
memory_limit = 256M
upload_max_filesize = 20M
post_max_size = 20M Paso 8: Instalar Composer
Composer es el gestor de dependencias de PHP: lee el archivo composer.json de tu proyecto y descarga las bibliotecas exactas que necesita. El paquete composer de los repositorios de Ubuntu funciona, pero suele ir por detrás de la versión oficial; instalarlo desde getcomposer.org garantiza la última estable.
cd ~
curl -sS https://getcomposer.org/installer -o composer-setup.php
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php
composer --version Paso 9: Descargar el proyecto
Clona el repositorio en /var/www/ y asigna la propiedad a tu usuario de despliegue, no a root. Si los archivos pertenecen a root, cada git pull posterior exigirá sudo y terminarás creando archivos con propietarios mezclados, que es el origen de la mitad de los problemas de permisos en Laravel.
sudo mkdir -p /var/www
sudo chown deploy:deploy /var/www
cd /var/www
git clone https://github.com/tu_usuario/tu_proyecto.git
cd tu_proyecto
composer install --no-dev --optimize-autoloader Las dos opciones de Composer son deliberadas: --no-dev omite herramientas de pruebas y depuración que no pintan nada en producción, y --optimize-autoloader genera un mapa de clases estático que evita que PHP busque archivos en disco en cada petición.
Paso 10: Configurar el archivo .env
El archivo .env guarda toda la configuración sensible de la aplicación y nunca debe estar en el repositorio Git. Se crea a partir de .env.example y se rellena directamente en el servidor.
cp .env.example .env
nano .env APP_NAME="Mi Aplicacion"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://tu_dominio.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_db
DB_USERNAME=laravel_user
DB_PASSWORD=tu_contraseña_segura APP_DEBUG=false es obligatorio en producción. Con true, cualquier error muestra al visitante la traza completa: rutas del servidor, fragmentos de código y, en algunos casos, variables de entorno con credenciales dentro. Es una filtración de información en toda regla.
Genera la clave de cifrado de la aplicación, que Laravel usa para las sesiones y los datos cifrados:
php artisan key:generate Paso 11: Ejecutar las migraciones
Las migraciones crean las tablas de la base de datos a partir del código del proyecto. En producción se ejecutan con --force, porque Laravel pide confirmación interactiva cuando detecta APP_ENV=production y en un script de despliegue no hay quién responda.
php artisan migrate --force Antes de ejecutarlas sobre una base con datos reales, haz una copia:
mysqldump -u laravel_user -p laravel_db > ~/backup_$(date +%F).sql Paso 12: Configurar el sitio virtual de Apache
El DocumentRoot debe apuntar a public/, nunca a la raíz del proyecto. Esta es la decisión de seguridad más importante de todo el despliegue: la raíz contiene .env con tus credenciales, y si es accesible por web cualquiera puede descargarlo.
sudo nano /etc/apache2/sites-available/tu_proyecto.conf <VirtualHost *:80>
ServerName tu_dominio.com
ServerAlias www.tu_dominio.com
DocumentRoot /var/www/tu_proyecto/public
<Directory /var/www/tu_proyecto/public>
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/tu_proyecto_error.log
CustomLog ${APACHE_LOG_DIR}/tu_proyecto_access.log combined
</VirtualHost> AllowOverride All permite que el .htaccess de Laravel funcione, y ese archivo es el que convierte cualquier URL en una petición a index.php. Sin él, la portada carga pero todas las demás rutas dan 404.
Activa el sitio, habilita la reescritura de URLs y desactiva el sitio por defecto:
sudo a2ensite tu_proyecto.conf
sudo a2enmod rewrite
sudo a2dissite 000-default.conf
sudo apache2ctl configtest
sudo systemctl reload apache2 Paso 13: Ajustar los permisos correctamente
Laravel solo necesita escribir en dos directorios: storage/ y bootstrap/cache/. Dar la propiedad de todo el proyecto al usuario del servidor web es innecesario y significa que un fallo en la aplicación permitiría reescribir su propio código.
sudo chown -R deploy:www-data /var/www/tu_proyecto
sudo find /var/www/tu_proyecto -type f -exec chmod 644 {} \;
sudo find /var/www/tu_proyecto -type d -exec chmod 755 {} \;
sudo chmod -R 775 /var/www/tu_proyecto/storage
sudo chmod -R 775 /var/www/tu_proyecto/bootstrap/cache
sudo chmod 640 /var/www/tu_proyecto/.env El esquema resultante: el usuario deploy es dueño de los archivos y puede desplegar sin sudo; el grupo www-data (bajo el que corre Apache) puede leer todo y escribir solo donde Laravel lo necesita; y .env no es legible para el resto de usuarios del sistema.
Paso 14: Activar el certificado SSL
Certbot obtiene el certificado de Let's Encrypt, reescribe tu configuración de Apache para servir HTTPS y deja programada la renovación automática. No hace falta añadir ninguna tarea de cron a mano: el paquete instala su propio temporizador de systemd.
sudo apt install certbot python3-certbot-apache -y
sudo certbot --apache -d tu_dominio.com -d www.tu_dominio.com Comprueba que la renovación quedó activa:
systemctl list-timers | grep certbot
sudo certbot renew --dry-run Este punto gana importancia cada año: Let's Encrypt está acortando la vida de sus certificados. El perfil clásico sigue emitiendo a 90 días, pero el perfil tlsserver bajó a 45 días el 13 de mayo de 2026 y el clásico pasará a 64 días el 10 de febrero de 2027. Cualquier proceso de renovación manual dejará de ser sostenible.
Paso 15: Optimizar Laravel para producción
Estos cuatro comandos son lo que separa un despliegue funcional de uno rápido, y son el paso que más se omite. Laravel, sin ellos, lee y analiza en cada petición todos los archivos de configuración, todas las rutas y todas las vistas Blade.
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache Qué hace cada uno: config:cache fusiona toda la configuración en un solo archivo PHP; route:cache compila la tabla de rutas; view:cache precompila las plantillas Blade; event:cache mapea los escuchadores de eventos.
Advertencia importante sobre config:cache: una vez ejecutado, Laravel deja de leer el archivo .env en tiempo de ejecución. Cualquier cambio posterior en .env no tendrá efecto hasta que ejecutes php artisan config:clear y vuelvas a cachear. Es la causa número uno de «cambié la contraseña de la base de datos y sigue fallando».
Si tu aplicación usa colas, añade también el trabajador en segundo plano con un gestor de procesos como Supervisor, para que sobreviva a reinicios:
sudo apt install supervisor -y Apache o Nginx: cuál elegir para Laravel
Apache con mod_php es más simple de configurar; Nginx con PHP-FPM aprovecha mejor la memoria en servidores pequeños. Ambos sirven Laravel perfectamente y la elección rara vez es el cuello de botella real de una aplicación.
| Criterio | Apache + mod_php | Nginx + PHP-FPM |
|---|---|---|
| Configuración inicial | Más simple, .htaccess funciona directo | Requiere bloque location con try_files |
| Consumo de RAM por petición | Mayor: PHP vive dentro de cada proceso | Menor: PHP corre en un pool aparte |
| Archivos estáticos | Correcto | Más eficiente |
| Configuración por directorio | Sí, con .htaccess | No, todo en el archivo del sitio |
| Muchas conexiones simultáneas | Aceptable | Mejor, modelo asíncrono |
| Cuándo conviene | Migras desde hosting compartido, usas .htaccess | VPS con poca RAM, mucho tráfico o muchos estáticos |
Si tu VPS tiene 1 o 2 GB de RAM y esperas tráfico real, Nginx con PHP-FPM te dará más margen. Si vienes de un hosting compartido y tu proyecto depende de reglas en .htaccess, Apache te ahorra una traducción.
Errores frecuentes tras el despliegue
| Síntoma | Causa habitual | Solución |
|---|---|---|
| Error 500 sin mensaje | Permisos de storage/ o clave sin generar | chmod -R 775 storage y php artisan key:generate |
| La portada carga, el resto da 404 | Falta a2enmod rewrite o AllowOverride All | Habilitar módulo y opción, recargar Apache |
| El navegador muestra el código PHP | Falta libapache2-mod-php o no está habilitado | Instalar el módulo y recargar Apache |
Cambié .env y no surte efecto | La configuración está cacheada | php artisan config:clear y volver a cachear |
SQLSTATE[HY000] [1045] | Credenciales incorrectas en .env | Probar con mysql -u laravel_user -p |
| Se ve el listado de archivos del proyecto | DocumentRoot apunta a la raíz, no a public/ | Corregirlo de inmediato: tu .env está expuesto |
| Fallos aleatorios de escritura | Archivos con propietarios mezclados por usar sudo | Reaplicar chown -R deploy:www-data |
Los logs que resuelven casi todo:
sudo tail -f /var/log/apache2/tu_proyecto_error.log
tail -f /var/www/tu_proyecto/storage/logs/laravel.log Cómo actualizar la aplicación sin romper nada
Un despliegue posterior no repite la instalación: actualiza el código, las dependencias y las cachés, en ese orden y con el sitio en modo mantenimiento. Guarda esta secuencia como script y úsala siempre igual.
cd /var/www/tu_proyecto
php artisan down
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan up php artisan down muestra a los visitantes una página de mantenimiento en lugar de errores a medio camino mientras las migraciones se aplican. php artisan up la retira. Si algo falla entre medias y el sitio queda en mantenimiento, php artisan up lo devuelve a la normalidad.
Poner una aplicación Laravel en producción implica coordinar sistema operativo, base de datos, PHP, servidor web y SSL, pero en el orden correcto el proceso es predecible y repetible. La checklist para futuros despliegues: actualizar y asegurar el VPS, crear usuario sin root y firewall con SSH permitido primero, instalar el stack con PHP 8.3 o superior, clonar el proyecto con composer install --no-dev --optimize-autoloader, configurar .env con APP_DEBUG=false, apuntar el DocumentRoot a public/, ajustar permisos solo donde hacen falta, activar HTTPS y cachear configuración, rutas y vistas. Los dos errores que más caro salen son dejar el DocumentRoot en la raíz del proyecto, que expone tu .env, y olvidar que config:cache congela la configuración hasta que la limpies. Si todavía estás eligiendo dónde alojar el proyecto, en hosting compartido vs VPS vs dedicado explicamos por qué Laravel necesita acceso a la línea de comandos y, por tanto, un VPS. Si tienes dudas durante la implementación, nuestro equipo de soporte técnico está disponible 24/7 para ayudarte.
Hosting con LiteSpeed, NVMe y soporte 24/7 desde $3/mes.