Docs menu
Troubleshooting PHP
Common build and runtime errors for PHP apps on Primcloud, with the fix for each.
On this page
Two places to look when a deploy misbehaves:
- Build log: the Deployments tab streams the full output from the PHP build, Composer, and any
scripts.compilecommands yourcomposer.jsondefines. - Runtime log: the Logs tab streams
stdoutandstderrfrom your running pods. PHP-FPM startup errors, fatal errors, and application-level exceptions show up here.
This page lists the errors that come up most often, in the order they'd typically bite you: detection, Composer install, extensions, then runtime.
Common build errors#
No composer.json found#
The build looked for composer.json at the repo root and didn't find one. Detection failed and the build stopped before any PHP-specific work started.
Fix. Add a composer.json at the root. An empty {} is enough to trigger detection, though a real app will have a require section. If your app lives in a subdirectory of a monorepo, set COMPOSER=path/to/composer.json in the Secrets panel to point the build at it, or open the subdirectory as its own Primcloud app.
No Composer lock file found#
composer.json declares dependencies in require, but there's no composer.lock alongside it. The build refuses to guess a version graph.
Fix. Run composer update locally to generate composer.lock, then commit and push:
composer update
git add composer.lock
git commit -m "Add composer.lock"
git push
If composer.lock is in .gitignore, remove it. The lockfile has to be checked in for reproducible deploys.
Failed to parse composer.json / composer.lock#
One of the files has a JSON syntax error, usually from a manual edit or an unresolved merge conflict.
Fix. Run composer validate locally to identify the exact line. For a botched lockfile, composer update --lock regenerates it from composer.json.
Your requirements could not be resolved to an installable set#
Composer couldn't find a combination of PHP version, extensions, and package versions that satisfies every constraint in your composer.json and composer.lock.
The build log includes the full resolver output above Primcloud's own error. That's the useful signal. Look for lines that mention the specific package or extension it couldn't place.
Fix.
- If it's a PHP version conflict (a dependency requires PHP 8.4 but you've pinned
"php": "^8.2"), bump the constraint andcomposer update. - If it's a missing extension (a dependency requires
ext-intland you haven't declared it), add it tocomposer.jsonandcomposer update. - If it's a version conflict between two dependencies, run
composer why-not <package> <version>locally to see what's blocking the resolution.
Runtime specified in require-dev but not in require#
require-dev names a PHP version (directly or via a dev-only dependency), but the main require doesn't. Composer can't pick a default runtime in this case even though --no-dev skips the dev packages at install time.
Fix. Add an explicit "php" entry to require in composer.json, run composer update, commit both files.
No matching PHP version#
The version constraint in composer.json doesn't match any PHP version the Primcloud Runtime ships. Either the constraint targets an unsupported major (PHP 7.x, PHP 9.x) or pins an exact patch that isn't shipped.
Fix. Bump to a version listed in Runtime versions. Loosen strict constraints ("8.4.7" becomes "^8.4") so the resolver has room to pick an available patch.
composer install failed#
Composer ran and exited non-zero. This is the catch-all for post-resolution failures: network issues, private repository auth failures, or a broken post-install-cmd.
Fix. Read the error output directly above Primcloud's summary line. Common causes:
- Auth failure against a private repository. Set
COMPOSER_AUTHin the Secrets panel; see Package managers → Private packages and registries. - A
post-install-cmdscript threw an error. The command's own output is in the log. Fix the underlying issue (missing file, unset env var, failed HTTP call) and redeploy.
Common runtime errors#
App doesn't bind to $PORT#
Symptom: the deployment reaches Release Succeeded but the URL returns a connection error, and the runtime log has no Now listening on: line.
Fix. Confirm the web process launcher is heroku-php-apache2 or heroku-php-nginx. Both bind to $PORT automatically. If you've replaced the launcher with a custom entrypoint (a php -S, a hand-rolled daemon), bind to $PORT explicitly:
$port = getenv('PORT') ?: '8080';
Document root not found#
The build succeeds but every request returns a 404 or an nginx/Apache default page.
Fix. Check the argument after the launcher in your Procfile:
web: heroku-php-nginx public/
The directory must exist relative to the repo root and contain the app's front controller (index.php for most frameworks). Laravel uses public/, Symfony uses public/, Slim commonly uses web/ or public/. A missing document root argument makes the launcher serve from the repo root, which is almost never what you want.
Fatal: Uncaught Error: Class not found#
PHP couldn't autoload a class at runtime, usually because the class isn't in the classmap generated at build time.
Fix. Add the missing PSR-4 namespace to autoload in composer.json, commit, and redeploy:
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
--optimize-autoloader runs at build time, so the classmap won't pick up files added after the deploy. If you're building assets or generating classes as part of scripts.compile, make sure they land before Composer finalizes the autoloader by running composer dump-autoload -o as the last compile step.
PHP Fatal error: Uncaught Error: Call to undefined function ext_*#
Your code calls a function from an extension that isn't loaded: intl, gd, redis, imagick, and so on.
Fix. Declare the extension in composer.json, run composer update to write it into the lockfile, commit, and redeploy:
{
"require": {
"php": "^8.4",
"ext-intl": "*",
"ext-gd": "*"
}
}
See Customizing the build → Native dependencies for the supported extension set.
Request terminated after 30 seconds#
PHP-FPM killed a request that ran past its request_terminate_timeout. The runtime log shows the worker restarting.
Fix. For legitimately long-running work (a report export, a bulk import, a webhook that fans out), move the work off the request path and into a queue. Add a worker process to your Procfile:
web: heroku-php-nginx public/
worker: php artisan queue:work
Then dispatch the slow work from the request handler and return immediately. For a one-off long-running operation (say, an initial data migration), the Tasks feature is the right shape.
Out of memory (OOM) at build time#
Composer exited with a memory allocation failure during install. This happens on complex dependency graphs (hundreds of packages with tight version constraints) when the PHP process needs more than the pod has.
Fix. Set COMPOSER_MEMORY_LIMIT=-1 in the Secrets panel to remove the cap entirely. Redeploy.
Debugging tips#
- Reproduce locally with the same commands.
composer install --no-dev --optimize-autoloader --prefer-diston your machine mirrors what the build runs. Most build failures reproduce this way. - Check the resolved PHP version. The build log's install phase prints the exact PHP patch it picked. Compare it to what your
composer.jsonandcomposer.lockasked for. - Turn up Composer verbosity temporarily. Add a
-v(or-vv/-vvv) to yourcompilescript's Composer invocations, redeploy, and Composer prints resolution details. Remove once you've isolated the issue. - Run one-off diagnostics from the Tasks feature.
php -mprints the loaded extensions in the runtime image;composer show -iprints the installed packages. Both are quick sanity checks when the build succeeded but the runtime is misbehaving.