PHP и Laravel
Deploy PHP and Laravel apps on Skiffly with Railpack — Composer, the PHP version and extensions, FrankenPHP on `PORT`, `APP_KEY`, MySQL or Postgres, Redis, migrations, queues, the scheduler and storage.
What Railpack detects#
| Looks at | Effect |
|---|---|
composer.json or index.php in the root | the service is a PHP app; composer install --no-dev --optimize-autoloader |
require.php in composer.json | the PHP version (default 8.4, minimum 8.2) |
artisan | Laravel: document root public/, storage/ and bootstrap/cache made writable, php artisan config:cache, route:cache, view:cache, event:cache, the storage:link symlink — and php artisan migrate --force at build time unless RAILPACK_SKIP_MIGRATIONS=true |
package.json next to the PHP code | Node is installed and npm run build compiles Vite/Mix assets; dev dependencies are pruned afterwards |
RAILPACK_PHP_EXTENSIONS=gd,intl,bcmath | extra PHP extensions |
RAILPACK_PHP_ROOT_DIR=web | a document root other than public/ (plain PHP: the repository root) |
The app is served by FrankenPHP (Caddy + PHP) from a start-container.sh Railpack provides; you can put your own start-container.sh, Caddyfile or php.ini in the repository root to override them. There is no separate nginx/php-fpm pair to configure. Composer's cache is kept between builds.
Port#
FrankenPHP listens on PORT (Skiffly sets it to the service port, 8080 when unset). Nothing to configure. If you use php artisan serve as the start command instead of the built-in server, pass --host=0.0.0.0 --port=$PORT; it is single-process and fine for small apps only.
Laravel variables#
Laravel reads .env through config/; on Skiffly the values come from service variables (do not commit .env). The minimum:
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 via the self reference follows the domain automatically; switch it to https://app.example.com once you add a custom domain. LOG_CHANNEL=stderr makes skiffly logs show the application log. Trust the edge proxy for https URLs: TrustProxies middleware with $proxies = '*' (Laravel 11: ->trustProxies(at: '*') in bootstrap/app.php).
Database and cache#
skiffly deploy --template mysql # or 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 accepts a single DB_URL (DATABASE_URL also works) instead of DB_HOST/DB_PORT/…; the Postgres template's DATABASE_URL uses the postgresql:// scheme, which Laravel maps to pgsql. REDIS_URL is parsed the same way. Both run on the private network; skiffly connect mysql opens a client from your machine.
Migrations, caches and the start command#
With RAILPACK_SKIP_MIGRATIONS=true, run migrations when the container starts. Set the start command (Settings → Deploy → Start command, startCommand):
sh -c "php artisan migrate --force && ./start-container.sh"./start-container.sh is Railpack's default entry point (FrankenPHP). Config/route/view caches were built during the build, so no optimize call is needed at runtime; if you change APP_URL or another config-backed variable, redeploy so the cache is rebuilt. One-offs: skiffly ssh -- php artisan migrate:status, skiffly ssh -- php artisan tinker.
Queues and the scheduler#
- Queue worker: a second service from the same repository, start command
php artisan queue:work --tries=3 --max-time=3600, no domain, the same variables through references (APP_KEY=${{web.APP_KEY}},DB_URL=${{MySQL.MYSQL_URL}}). Horizon:php artisan horizon. - Scheduler: a service with cron schedule
* * * * *and the start commandphp artisan schedule:run— Skiffly starts a container each minute and waits for it to exit; runs do not overlap. Orphp artisan schedule:workas a long-running worker service.
Storage and uploads#
storage/app is on the ephemeral disk. For user uploads mount a volume (skiffly volume add --mount-path /app/storage/app, one replica) or use FILESYSTEM_DISK=s3 with the minio template or an external bucket. storage:link is created at build time, so public/storage works for files on a mounted volume too.
Health check#
Laravel 11 ships GET /up. Set the healthcheck path to /up (older versions: a route that returns 200).
Common problems#
| Symptom | Fix |
|---|---|
No application encryption key has been specified | set APP_KEY (base64: + 32 random bytes) |
Build hangs or fails on php artisan migrate | RAILPACK_SKIP_MIGRATIONS=true, migrate from the start command |
SQLSTATE[HY000] [2002] Connection refused | DB_URL reference missing, or the database service is still starting (skiffly status) |
Mixed-content or http:// links | APP_URL with https://, trust proxies |
419 Page Expired on forms | session driver misconfigured (SESSION_DRIVER=redis with REDIS_URL) or APP_URL domain mismatch |
Vite assets missing (Vite manifest not found) | package.json must be in the root so Railpack runs npm run build; check skiffly logs --build |
The stream or file "storage/logs/laravel.log" could not be opened | LOG_CHANNEL=stderr |
| Changed a config variable but the app ignores it | config cache is built at build time: deploy again |
Example#
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() /* set once with `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