PHP y Laravel
Despliega apps de PHP y Laravel en Skiffly con Railpack — Composer, la versión de PHP y las extensiones, FrankenPHP en `PORT`, `APP_KEY`, MySQL o Postgres, Redis, migraciones, colas, el scheduler y el almacenamiento.
Qué detecta Railpack#
| Mira | Efecto |
|---|---|
composer.json o index.php en la raíz | el servicio es una app de PHP; composer install --no-dev --optimize-autoloader |
require.php en composer.json | la versión de PHP (por defecto 8.4, mínimo 8.2) |
artisan | Laravel: document root public/, storage/ y bootstrap/cache con permisos de escritura, php artisan config:cache, route:cache, view:cache, event:cache, el symlink de storage:link — y php artisan migrate --force durante el build salvo que RAILPACK_SKIP_MIGRATIONS=true |
package.json junto al código PHP | se instala Node y npm run build compila los assets de Vite/Mix; después se eliminan las dependencias de desarrollo |
RAILPACK_PHP_EXTENSIONS=gd,intl,bcmath | extensiones de PHP adicionales |
RAILPACK_PHP_ROOT_DIR=web | un document root distinto de public/ (PHP plano: la raíz del repositorio) |
La app se sirve con FrankenPHP (Caddy + PHP) desde un start-container.sh que provee Railpack; puedes poner tu propio start-container.sh, Caddyfile o php.ini en la raíz del repositorio para reemplazarlos. No hay un par nginx/php-fpm separado que configurar. La caché de Composer se conserva entre builds.
Puerto#
FrankenPHP escucha en PORT (Skiffly le asigna el puerto del servicio, 8080 cuando no se define). No hay nada que configurar. Si usas php artisan serve como comando de inicio en lugar del servidor integrado, pásale --host=0.0.0.0 --port=$PORT; es de un solo proceso y solo sirve para apps pequeñas.
Variables de Laravel#
Laravel lee .env a través de config/; en Skiffly los valores vienen de las variables del servicio (no hagas commit de .env). Lo mínimo:
skiffly variables set --skip-deploys \
APP_ENV=production APP_DEBUG=false \
APP_KEY="base64:$(openssl rand -base64 32)" \
'APP_URL=https://${{self.SKIFFLY_PUBLIC_DOMAIN}}' \
LOG_CHANNEL=stderr \
RAILPACK_SKIP_MIGRATIONS=trueAPP_URL mediante la referencia self sigue al dominio automáticamente; cámbiala a https://app.example.com cuando agregues un dominio propio. LOG_CHANNEL=stderr hace que skiffly logs muestre el log de la aplicación. Confía en el proxy de borde para las URL https: middleware TrustProxies con $proxies = '*' (Laravel 11: ->trustProxies(at: '*') en bootstrap/app.php).
Base de datos y caché#
skiffly deploy --template mysql # o postgres
skiffly deploy --template redis
skiffly variables set 'DB_URL=${{MySQL.MYSQL_URL}}' 'REDIS_URL=${{Redis.REDIS_URL}}' \
CACHE_STORE=redis SESSION_DRIVER=redis QUEUE_CONNECTION=redisLaravel acepta un único DB_URL (DATABASE_URL también funciona) en lugar de DB_HOST/DB_PORT/…; el DATABASE_URL de la plantilla de Postgres usa el esquema postgresql://, que Laravel asocia a pgsql. REDIS_URL se interpreta de la misma forma. Ambos corren en la red privada; skiffly connect mysql abre un cliente desde tu máquina.
Migraciones, cachés y el comando de inicio#
Con RAILPACK_SKIP_MIGRATIONS=true, ejecuta las migraciones cuando arranca el contenedor. Define el comando de inicio (Configuración → Despliegue → Comando de inicio, startCommand):
sh -c "php artisan migrate --force && ./start-container.sh"./start-container.sh es el punto de entrada por defecto de Railpack (FrankenPHP). Las cachés de config/rutas/vistas se generaron durante el build, así que no hace falta ninguna llamada a optimize en runtime; si cambias APP_URL u otra variable respaldada por la config, vuelve a desplegar para que la caché se regenere. Comandos puntuales: skiffly ssh -- php artisan migrate:status, skiffly ssh -- php artisan tinker.
Colas y el scheduler#
- Worker de colas: un segundo servicio del mismo repositorio, comando de inicio
php artisan queue:work --tries=3 --max-time=3600, sin dominio, las mismas variables mediante referencias (APP_KEY=${{web.APP_KEY}},DB_URL=${{MySQL.MYSQL_URL}}). Horizon:php artisan horizon. - Scheduler: un servicio con programación cron
* * * * *y el comando de iniciophp artisan schedule:run— Skiffly arranca un contenedor cada minuto y espera a que salga; las ejecuciones no se solapan. Ophp artisan schedule:workcomo servicio worker de larga duración.
Almacenamiento y subidas de archivos#
storage/app está en el disco efímero. Para los archivos que suben los usuarios, monta un volumen (skiffly volume add --mount-path /app/storage/app, una réplica) o usa FILESYSTEM_DISK=s3 con la plantilla minio o un bucket externo. storage:link se crea durante el build, así que public/storage también funciona para archivos en un volumen montado.
Health check#
Laravel 11 incluye GET /up. Define /up como ruta del health check (versiones anteriores: una ruta que devuelva 200).
Problemas comunes#
| Síntoma | Solución |
|---|---|
No application encryption key has been specified | define APP_KEY (base64: + 32 bytes aleatorios) |
El build se cuelga o falla en php artisan migrate | RAILPACK_SKIP_MIGRATIONS=true, migra desde el comando de inicio |
SQLSTATE[HY000] [2002] Connection refused | falta la referencia DB_URL, o el servicio de base de datos todavía está arrancando (skiffly status) |
Contenido mixto o enlaces http:// | APP_URL con https://, confía en los proxies |
419 Page Expired en formularios | driver de sesión mal configurado (SESSION_DRIVER=redis con REDIS_URL) o el dominio de APP_URL no coincide |
Faltan los assets de Vite (Vite manifest not found) | package.json debe estar en la raíz para que Railpack ejecute npm run build; revisa skiffly logs --build |
The stream or file "storage/logs/laravel.log" could not be opened | LOG_CHANNEL=stderr |
| Cambiaste una variable de config pero la app la ignora | la caché de config se genera durante el build: despliega de nuevo |
Ejemplo#
import { defineSkiffly, github, mysql, preserve, project, redis, service } from "@skiffly/config";
export default defineSkiffly(() => {
const db = mysql("db");
const cache = redis("cache");
const env = {
APP_ENV: "production", APP_DEBUG: "false", LOG_CHANNEL: "stderr", RAILPACK_SKIP_MIGRATIONS: "true",
DB_URL: db.env.MYSQL_URL, REDIS_URL: cache.env.REDIS_URL, CACHE_STORE: "redis", SESSION_DRIVER: "redis", QUEUE_CONNECTION: "redis",
};
const web = service("web", {
source: github("acme/shop"),
start: 'sh -c "php artisan migrate --force && ./start-container.sh"',
healthcheck: "/up",
env: { ...env, APP_KEY: preserve() /* se define una sola vez con `skiffly variables set` */, APP_URL: "https://${{self.SKIFFLY_PUBLIC_DOMAIN}}" },
domain: true,
});
const queue = service("queue", { source: github("acme/shop"), start: "sh -c 'php -S 0.0.0.0:$PORT -t /tmp & exec php artisan queue:work --tries=3'", env: { ...env, APP_KEY: web.env.APP_KEY, APP_URL: web.env.APP_URL } });
const scheduler = service("scheduler", { source: github("acme/shop"), start: "php artisan schedule:run", cron: "* * * * *", env: { ...env, APP_KEY: web.env.APP_KEY, APP_URL: web.env.APP_URL } });
return project("shop", { resources: [db, cache, web, queue, scheduler] });
});Landing: /deploy/php-laravel