Curso de Laravel

Desplegar un proyecto Laravel en un hosting compartido

Por Víctor Peña · Publicado el

Hola, ¿cómo están? Ya tenemos el sistema completo. Falta lo que más nervios da: ponerlo en internet.

El hosting compartido no es el mejor sitio para Laravel —un servidor propio o una plataforma pensada para ello dan mucho menos problema— pero es lo que hay disponible y lo que la mayoría de clientes ya tiene contratado. Y funciona perfectamente, si se conocen los cuatro puntos que se atragantan.

¡Empecemos!

Antes de subir nada

Comprobar la versión de PHP

Laravel 13 necesita PHP 8.3 o superior. En el panel del hosting suele haber un selector de versión.

Si el hosting solo ofrece PHP 8.1, no lo intentes: cambia de hosting o usa una versión anterior de Laravel. Forzarlo termina siempre mal.

Las extensiones necesarias: bcmath, ctype, curl, dom, fileinfo, json, mbstring, openssl, pcre, pdo, tokenizer, xml, y zip si vas a usar Excel.

Preparar el proyecto

composer install --optimize-autoloader --no-dev
npm run build

Ese --no-dev deja fuera las herramientas de desarrollo. Y npm run build es obligatorio: en producción no corre Vite, así que sin los archivos compilados el sistema sale sin estilos.

Revisar el .gitignore

/node_modules
/public/build
/public/storage
/storage/*.key
/vendor
.env

El .env nunca va al repositorio. Tiene la contraseña de la base de datos y la clave de la aplicación.

El problema de la carpeta public

Aquí está la particularidad de Laravel en hosting compartido.

Laravel espera que la raíz del sitio sea public/, y que todo lo demás quede fuera del alcance del navegador. Un hosting compartido, en cambio, sirve directamente public_html/.

Si subes todo el proyecto dentro de public_html/, cualquiera puede abrir tudominio.com/.env y leer tus credenciales. Es una fuga total, y pasa más de lo que parece.

Hay tres formas de resolverlo, de mejor a peor.

Opción 1: cambiar la raíz del dominio

Si el panel permite elegir la carpeta raíz del dominio, es la solución limpia:

/home/usuario/
├── clinica/              ← todo el proyecto
│   ├── app/
│   ├── public/           ← raíz del dominio
│   ├── vendor/
│   └── .env

Apuntas el dominio a /home/usuario/clinica/public y listo. Sin trucos, sin nada raro.

Opción 2: separar public del resto

Cuando no se puede cambiar la raíz:

/home/usuario/
├── clinica/              ← proyecto sin la carpeta public
│   ├── app/
│   ├── vendor/
│   └── .env
└── public_html/          ← el contenido de public/
    ├── index.php
    ├── .htaccess
    └── build/

Y hay que ajustar public_html/index.php:

require __DIR__.'/../clinica/vendor/autoload.php';

$app = require_once __DIR__.'/../clinica/bootstrap/app.php';

Funciona bien. El único inconveniente es que hay que recordar ese cambio en cada despliegue.

Opción 3: todo en public_html con .htaccess

La que no recomiendo, pero a veces es lo único posible:

RewriteEngine On
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule ^(.*)$ public/$1 [L]

Y protegiendo lo sensible:

<FilesMatch "^\.env|composer\.(json|lock)$">
    Require all denied
</FilesMatch>

Depende de que ese .htaccess no falle nunca. Si alguien lo borra por accidente, todo el código queda expuesto. Úsala solo si no hay alternativa.

Subir los archivos

Si el hosting tiene acceso SSH y Git, es lo más cómodo:

cd ~/clinica
git clone https://github.com/usuario/clinica.git .
composer install --optimize-autoloader --no-dev

Es lo que vimos en la lección de GitHub, y hace que actualizar sea un git pull.

Si solo hay FTP, comprime el proyecto en local —incluyendo vendor/, porque sin Composer en el servidor no puedes instalarlo allá— y descomprime desde el administrador de archivos del panel. Subir 20.000 archivos sueltos por FTP tarda horas.

Configurar el .env

APP_NAME="Clínica San Rafael"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://clinica.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=usuario_clinica
DB_USERNAME=usuario_clinica
DB_PASSWORD=la-contraseña

SESSION_DRIVER=database
QUEUE_CONNECTION=database
CACHE_STORE=database

MAIL_MAILER=smtp
MAIL_HOST=mail.clinica.com
MAIL_PORT=587
MAIL_USERNAME=sistema@clinica.com
MAIL_PASSWORD=...
MAIL_FROM_ADDRESS="sistema@clinica.com"

APP_LOCALE=es
APP_TIMEZONE=America/La_Paz

APP_DEBUG=false es lo más importante de todo el archivo. Con true, cualquier error muestra la traza completa: rutas del servidor, fragmentos de código y, en algunos casos, las variables de entorno con las contraseñas. Es la fuga de información más común en proyectos Laravel mal desplegados.

Y generar la clave:

php artisan key:generate

Sin ella, las sesiones y todo lo cifrado dejan de funcionar. Si no tienes SSH, genérala en local y cópiala.

Nunca cambies APP_KEY en un sistema en producción: todo lo cifrado con la anterior deja de poder descifrarse.

Fíjate también en DB_HOST=127.0.0.1. En hosting compartido casi nunca es localhost con un nombre de servidor externo, aunque el panel muestre otra cosa.

La base de datos

Se crea desde el panel del hosting, junto con el usuario y sus permisos.

php artisan migrate --force

Ese --force es obligatorio en producción; sin él, Artisan pide confirmación y el comando se queda esperando.

Si no hay SSH, exporta la base de datos desde local y súbela por el administrador de bases de datos del panel:

php artisan migrate
mysqldump -u root clinica > clinica.sql

Y nunca ejecutes migrate:fresh en producción. Borra todas las tablas. Es un comando que en un servidor con datos reales no debería escribirse nunca.

Permisos

chmod -R 775 storage bootstrap/cache

Esas dos carpetas son las únicas que Laravel necesita escribir. Si algún tutorial dice chmod -R 777 a todo el proyecto, ignóralo: eso permite que cualquier proceso del servidor modifique tu código.

Si el hosting corre PHP con tu propio usuario, con 755 suele bastar.

El enlace de storage

php artisan storage:link

Es el paso de la lección de archivos que más se olvida al desplegar. Sin él, las imágenes que en local se veían perfectamente salen rotas en producción.

Si el hosting no permite enlaces simbólicos, se puede cambiar la raíz del disco público en config/filesystems.php para que apunte directamente a una carpeta dentro de public/.

Optimizar

php artisan optimize

Cachea configuración, rutas y vistas. Es lo que vimos en la lección de caché, y en producción sí se hace, al contrario que en desarrollo.

Recuerda la consecuencia: con la configuración cacheada, env() fuera de config/ devuelve null. Si el sistema funcionaba en local y en producción falla algo de configuración, esa es la primera sospecha.

HTTPS

Casi todos los hosting incluyen un certificado gratuito activable desde el panel.

Y hay que forzar la redirección. En AppServiceProvider:

public function boot(): void
{
    if ($this->app->environment('production')) {
        URL::forceScheme('https');
    }
}

Sin eso, los enlaces generados por route() pueden salir en http y el navegador bloquea el contenido mixto.

Cron y colas

Una sola línea, con la ruta absoluta al binario de PHP correcto:

* * * * * /usr/local/bin/php /home/usuario/clinica/artisan schedule:run >> /dev/null 2>&1

Para saber cuál es la ruta:

which php8.3

El PHP del cron y el de la web muchas veces son versiones distintas en hosting compartido, y eso da errores desconcertantes. Vale la pena verificarlo.

Y para las colas, el truco de la lección de tareas programadas:

Schedule::command('queue:work --stop-when-empty --max-time=55')
    ->everyMinute()
    ->withoutOverlapping();

Respaldos

Antes de dar el sistema por entregado:

composer require spatie/laravel-backup
Schedule::command('backup:clean')->daily()->at('01:00');
Schedule::command('backup:run')->daily()->at('02:00');

Y comprueba que el respaldo realmente se genera y se puede restaurar. Un respaldo que nadie ha probado no es un respaldo. La notificación en caso de fallo que vimos en la lección de tareas programadas es lo mínimo.

La secuencia completa de despliegue

Para las actualizaciones siguientes:

php artisan down --secret="mi-clave-secreta"

git pull origin main
composer install --no-dev --optimize-autoloader

php artisan migrate --force

php artisan optimize:clear
php artisan optimize

php artisan queue:restart

php artisan up

Ese --secret permite que tú sigas viendo el sistema mientras está en mantenimiento, entrando por tudominio.com/mi-clave-secreta. Muy útil para verificar antes de abrirlo a todos.

El orden importa: limpiar antes de cachear, y queue:restart después de todo.

Cuando algo falla

Error 500 sin más información

Primero, mira el log:

tail -50 storage/logs/laravel.log

Las causas más frecuentes, en orden:

  1. Permisos en storage/ o bootstrap/cache/
  2. Falta APP_KEY
  3. Versión de PHP incorrecta
  4. Falta una extensión
  5. Configuración en caché apuntando a valores viejos

Si necesitas ver el error de verdad, activa APP_DEBUG=true un momento, mira, y vuelve a ponerlo en false. No lo dejes activado «hasta que se estabilice».

Página en blanco

Casi siempre memoria agotada o un error fatal de PHP. Revisa el log de errores del hosting, que es distinto al de Laravel.

Todo da 404 menos la portada

mod_rewrite desactivado o el .htaccess de public/ sin subir. Es un archivo oculto, y muchos clientes FTP no lo muestran por defecto.

Sin estilos

Faltó npm run build, o APP_URL está mal.

Las imágenes no se ven

storage:link.

Los correos no salen

El hosting bloquea el puerto 587 o exige usar su propio servidor SMTP. Es una restricción habitual en hosting compartido.

La tarea programada no corre

Ruta de PHP incorrecta en el cron. Comprueba con php artisan schedule:list cuál es la próxima ejecución esperada.

Lista de verificación

Antes de decir que está listo:

  • APP_DEBUG=false
  • APP_ENV=production
  • APP_KEY generada
  • El .env no es accesible desde el navegador
  • HTTPS activo y forzado
  • php artisan optimize ejecutado
  • storage:link hecho
  • Permisos de storage y bootstrap/cache
  • Migraciones aplicadas
  • Cron configurado y verificado
  • Correo probado con un envío real
  • Respaldo automático y restauración probada
  • Zona horaria correcta
  • Página 404 y 500 personalizadas

Ese punto del .env se comprueba en diez segundos: abre tudominio.com/.env en el navegador. Debe dar 404 o 403. Si descarga un archivo, para todo y arregla eso antes que nada.

Cuándo dejar el hosting compartido

Vale la pena ser honesto sobre los límites.

El hosting compartido está bien para un sistema interno con pocos usuarios simultáneos, sin procesos pesados y con un presupuesto ajustado.

Se queda corto cuando necesitas trabajadores de cola permanentes, Redis, WebSockets, control de la versión de PHP, o despliegues automatizados. Ahí un servidor virtual o una plataforma gestionada cuesta poco más y ahorra muchísimo trabajo.

Si el sistema es para un cliente que va a depender de él, ese salto se paga solo.

Errores comunes

  • APP_DEBUG=true en producción.
  • Todo el proyecto dentro de public_html/, con el .env accesible.
  • Olvidar npm run build.
  • chmod 777 a todo.
  • migrate:fresh en un servidor con datos reales.
  • No ejecutar storage:link.
  • Ruta de PHP equivocada en el cron.
  • Cachear antes de limpiar.
  • Respaldos que nadie ha probado restaurar.
  • Cambiar APP_KEY en un sistema en marcha.

Para cerrar

Desplegar Laravel en hosting compartido es un proceso mecánico: si sigues los pasos en orden, funciona a la primera. Los problemas casi siempre vienen de saltarse uno.

De todo lo anterior, dos cosas son de seguridad y no admiten excepción: APP_DEBUG=false y el .env fuera del alcance del navegador. Lo demás son molestias; esas dos son fugas de información reales.

En la siguiente lección empezamos el último módulo del curso: inteligencia artificial dentro de Laravel.

Saludos y éxitos.

Norvic Software

Desarrollamos el software que tu empresa necesita

Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.

Solicitar cotizaciónVer todos los servicios

Cotización sin costo · Respuesta directa por WhatsApp