Ir al contenido

Buscar en la documentación

Busca en la documentación de Skiffly

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#

MiraEfecto
composer.json o index.php en la raízel servicio es una app de PHP; composer install --no-dev --optimize-autoloader
require.php en composer.jsonla versión de PHP (por defecto 8.4, mínimo 8.2)
artisanLaravel: 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 PHPse 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,bcmathextensiones de PHP adicionales
RAILPACK_PHP_ROOT_DIR=webun 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=true

APP_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=redis

Laravel 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 inicio php artisan schedule:run — Skiffly arranca un contenedor cada minuto y espera a que salga; las ejecuciones no se solapan. O php artisan schedule:work como 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íntomaSolución
No application encryption key has been specifieddefine APP_KEY (base64: + 32 bytes aleatorios)
El build se cuelga o falla en php artisan migrateRAILPACK_SKIP_MIGRATIONS=true, migra desde el comando de inicio
SQLSTATE[HY000] [2002] Connection refusedfalta 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 formulariosdriver 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 openedLOG_CHANNEL=stderr
Cambiaste una variable de config pero la app la ignorala caché de config se genera durante el build: despliega de nuevo

Ejemplo#

.skiffly/skiffly.ts
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