Docs menu
Deploying Laravel
Deploy a Laravel app on Primcloud: Procfile shape, required env vars, config caching, migrations, and the framework-specific gotchas.
On this page
Laravel deploys on Primcloud like any other PHP app: composer.json at the root, composer install runs at build, and a Procfile launcher starts the web process. The framework-specific work is picking the right document root, setting APP_KEY, deciding where to run migrations, and choosing session/cache/log drivers that don't rely on the local filesystem.
Starter template#
A fresh Laravel app scaffolded with the standard installer deploys with only a Procfile addition. To scaffold locally:
composer create-project laravel/laravel my-app
cd my-app
git init -b main
git add .
git commit -m "Initial Laravel scaffold"
Push the repo to a git provider your Primcloud account is connected to, then import it from the dashboard as described in Getting started with PHP → Create the application.
Procfile for Laravel on Primcloud#
Laravel serves from a public/ document root. The minimum Procfile is one line:
web: heroku-php-nginx public/
Or, if you prefer Apache with PHP-FPM:
web: heroku-php-apache2 public/
For a typical Laravel app that runs queued jobs, add a worker process type for the queue:
web: heroku-php-nginx public/
worker: php artisan queue:work --tries=3
Each entry becomes its own process type in Pod Formations, so scale the worker independently of web.
Env vars this framework expects#
Set these in the Secrets panel before your first deploy:
| Variable | Value | Purpose |
|---|---|---|
APP_KEY |
base64:... from php artisan key:generate --show |
Encryption key for sessions and encrypted casts. Laravel refuses to boot without one. |
APP_ENV |
production |
Enables production optimizations, disables debug pages. |
APP_DEBUG |
false |
Never true on a deployed app; it leaks stack traces and env values. |
APP_URL |
https://your-app.primcloud.app |
Used for absolute URLs in mail, signed URLs, and Sanctum stateful auth. |
LOG_CHANNEL |
stderr |
Ships logs to stdout/stderr so they appear in the Logs page. See below. |
SESSION_DRIVER |
database or redis |
The default file driver writes under storage/framework/sessions, which is ephemeral. |
CACHE_STORE |
database or redis |
Same reason: the default file cache doesn't survive a redeploy or a pod restart. |
QUEUE_CONNECTION |
database or redis |
Required if you're running the worker process above. |
DB_* |
Your database connection details | Set the full DB_HOST / DB_PORT / DB_DATABASE / DB_USERNAME / DB_PASSWORD group, or a DATABASE_URL if you prefer. |
Generating APP_KEY. Run this locally, then paste the output as the value for APP_KEY:
php artisan key:generate --show
Framework gotchas on Primcloud#
Config, route, and view caches. Laravel's caches (config:cache, route:cache, view:cache, event:cache) speed up production by pre-compiling boot-time work. Run them at build time via scripts.compile in composer.json so the caches are baked into the runtime image:
{
"scripts": {
"compile": [
"php artisan config:cache",
"php artisan route:cache",
"php artisan view:cache",
"php artisan event:cache"
]
}
}
Important
config:cache reads every env var used by config files, so make sure the environment has the values it needs before the build runs. An env var set for the first time on the same deploy that adds config:cache won't be visible during the build.
Front-end asset builds. Laravel's package.json is picked up automatically. After Composer finishes, Primcloud installs your JavaScript dependencies and runs the build script (vite build in a stock Laravel app). Commit the lockfile for the package manager you use: package-lock.json, yarn.lock, or pnpm-lock.yaml for npm, Yarn, or pnpm, or bun.lock to install and build with Bun. Because Composer runs first, vendor/ is already populated when the asset build scans it. See Customizing the build → Build hooks.
Vite writes its manifest to public/build/, which ships in the runtime image and is served by the launcher without any extra config.
Migrations. Don't run php artisan migrate from scripts.compile. That runs at build time, before your database credentials are guaranteed to be in the environment, and it would run again on every rebuild rather than once per deploy. You have two places to run migrations on Primcloud:
A release process in your Procfile runs once per deploy, after the build finishes and before the new pods take traffic. It's the cleanest fit for migrations: no manual step, and the deploy carries the schema change with it:
web: heroku-php-nginx public/
release: php artisan migrate --force
worker: php artisan queue:work --tries=3
The release process isn't a long-running pod and doesn't scale. It runs once each deploy, and a non-zero exit fails the deploy before the rollout, so a broken migration never reaches your pods. A release process that runs past its time limit fails the deploy the same way. Its output shows up in the Logs page under the release phase.
Alternatively, run the migration as a one-off from the Tasks feature:
php artisan migrate --force
A Task is the better choice for a migration you want to run on your own schedule (a large backfill you'd rather gate manually, or a one-time data fix) rather than automatically on every deploy.
Ephemeral filesystem. Every pod restart or redeploy starts from a fresh copy of the built image. Files written to storage/app/, storage/framework/, and storage/logs/ don't persist. Use an S3-compatible bucket for user uploads (FILESYSTEM_DISK=s3), a database or Redis for sessions and cache, and stdout/stderr for logs.
Logging to the platform. Set LOG_CHANNEL=stderr (Laravel 8+) or configure a custom channel that writes to php://stderr. Anything written to stdout or stderr from your pods streams into the Logs page. Writing to storage/logs/laravel.log works, but nothing on Primcloud is going to read it.
Trusted proxies. Requests reach your pods through Primcloud's ingress, which terminates TLS and sets X-Forwarded-* headers. Laravel needs to trust those to generate correct absolute URLs and detect HTTPS. In app/Http/Middleware/TrustProxies.php (or the equivalent in newer Laravel versions), set:
protected $proxies = '*';
Without this, url() helpers return http:// links even for HTTPS requests.
Troubleshooting#
No application encryption key has been specified.: APP_KEY isn't set. Generate one with php artisan key:generate --show locally and set it in the Secrets panel.
SQLSTATE[HY000] [2002] Connection refused right after deploy. The build succeeded but the app can't reach the database. Check DB_HOST, DB_PORT, and any network rules on the database side (allowlisting Primcloud's egress).
Assets 404 in production but load locally. Vite's manifest wasn't built or wasn't included in the deploy. Confirm package.json has a build script and a committed lockfile, and check the build log for the asset build's output. public/build/manifest.json must exist in the runtime image.
Session data disappears between requests. SESSION_DRIVER=file: sessions are being written to the ephemeral filesystem and lost on the next request routed to a different pod. Switch to database or redis.
Migrations silently didn't run. Nothing migrates your schema on its own. Add a release: php artisan migrate --force process to your Procfile to run it automatically each deploy, or invoke it from a Task by hand. Config-cache and route-cache are safe to bake into the image at build time; database schema changes are not.
Queued jobs never process. The worker process from your Procfile needs to be scaled to at least one pod. Adjust the formation from the dashboard; see Pod Formations.