Upgrading PHP and Laravel on Elastic Beanstalk
Once an environment is described by applicationPlatformArn, a platform or PHP upgrade is
a one-line PR that Elastic Beanstalk applies in place. The environment id, CNAME and
database stay the same. If production still passes applicationSolutionStackName, do
Migrating an Elastic Beanstalk Environment to PlatformArn
first. For an environment with no IaC at all, see
Environments with no IaC.
The upgrade order
Keep the platform ARN in one place in your provision app (Prism and the Offer API use
an eb-platform.ts module). Give each change below its own release, with nothing else
pending on main, so a regression has one cause.
- Bump the platform version, keep PHP. For example, PHP 8.2 on 4.7.6 to PHP 8.2 on 4.13.5. If you're several minors behind, this picks up all the OS, nginx and PHP patch releases at once, and it makes PHP the only variable in the next step.
- Bump PHP, keep the platform version. For example, PHP 8.2 to PHP 8.4, both on 4.13.5.
cdk diffshould show onlyPlatformArnchanging, with no replacement. - Build on the new PHP in CI. Bump
PHP_VERSIONin your build, release and artifact workflows after production runs the new version and has soaked. Move local dev images and the devcontainer in the same PR, and keep a non-required CI job on the old PHP while rollback is open. If yourDockerfilebuildsFROMa:latestbase, Docker reuses a cached copy and developers keep running the old PHP. Setpull: trueunder the Composebuild:key, or rebuild withdocker compose build --pull. - Raise the floor. Bump
require.phpincomposer.json, drop anyconfig.platform.phppin, and update the docs last. Whilecomposer.jsonstill allows the old version, a rollback stays open.
Before step 2:
- Run CI on both PHP versions. It catches breakage before the flip. PHPStan below 1.12 crashes at startup on 8.4, so bump it first or the 8.4 job tells you nothing.
- Find packages that cap the PHP version with
composer why-not php 8.4. - Update the capped packages on the old PHP. Run
composer updatewith Composer on the version production runs today, or pinconfig.platform.phpto it and leave the pin in place until step 4. Resolved on 8.4, Composer can pick versions that require 8.4, which quietly closes the rollback. - Read
.platform/hooksfor anything that assumes the old version.
Check what's available before picking a version:
aws elasticbeanstalk list-platform-versions --region <REGION> \
--filters Type=PlatformName,Operator=contains,Values=PHP \
--query "PlatformSummaryList[?contains(PlatformArn, 'Amazon Linux 2023')].PlatformArn" \
--output text | tr '\t' '\n' | sort
Availability depends on the account. An account sees the newest version of each branch, plus any older version it already has environments on.
Build and runtime order
Elastic Beanstalk skips composer install when the bundle ships vendor/, which ours do.
So vendor/ is built on CI's PHP and runs on the platform's PHP. Move the runtime first,
then the build.
Proving it moved
A green health status doesn't prove PHP changed. Take a php -m baseline before the deploy,
then check:
-
describe-environmentsreports the newPlatformArn. -
The instance AMI name contains the new branch, for example
eb_php84.The first two work with the read-only role. The
phpchecks need SSM access. -
php -vover SSM on an instance of each tier reports the new version. -
php -mover SSM, diffed against a baseline taken before the deploy, shows only the modules you expect to change. That's howpspelldropping out of 8.4 was caught. -
The health check and a real request return 200.
-
The Datadog tracer is loaded. It's the most likely thing a platform hook breaks.
-
Queue workers (Horizon) and the scheduler are running.
-
Error rate and p95 latency in Datadog match the day before.
If your role can call describe-configuration-settings, snapshot the option settings to
before.json before the deploy, with the same command, and diff them after:
aws elasticbeanstalk describe-configuration-settings --region <REGION> \
--application-name <APPLICATION_NAME> --environment-name <ENVIRONMENT_NAME> \
| jq -S '.ConfigurationSettings[0].OptionSettings | sort_by(.Namespace, .OptionName)' > after.json
diff before.json after.json
On a PHP-only bump, expect three changes: ImageId, and AppSource and HooksPkgUrl
moving from eb_php82 to eb_php84.
Rollback
Revert the PHP bump PR. It's the same in-place update in the other direction. After step 3,
revert the CI bump first and release it, so an old-PHP build is running before the platform
goes back. Otherwise you land in the "never" state above. Rollback closes at step 4: once
composer.json requires the new PHP, Composer's platform_check.php refuses to boot on the
old one.
Gotchas
- Health flips to Degraded mid-deploy. "Incorrect application version found on 1 out of 2 instances" is normal during any rollout and clears in a few minutes. Trust instance health and HTTP over the color.
- The nginx version isn't proof of a PHP change. It moves with the platform version, not the PHP branch.
- Code that only works because of dev dependencies. A config file that references a
require-devpackage is harmless whilevendor/ships whole and fatal if anything switches tocomposer install --no-dev. - Platform hooks hardcode things. Secret names, extension installers and PHP paths in
.platform/hookscan assume one environment or one PHP version.
PHP or Laravel first?
At every step, the Laravel version you run has to support the PHP version you run. When your current Laravel doesn't support the target PHP, use a bridge: a Laravel version that supports both your current PHP and the target, and that still has security support.
| Laravel | Supported PHP |
|---|---|
| 10 | 8.1 to 8.3 |
| 11 | 8.2 to 8.4 |
| 12 | 8.2 to 8.5 |
| 13 | 8.3 to 8.5 |
Laravel 10 and 11 are past their security support, so pass through them, don't stop on them. Check the Laravel support policy and PHP supported versions for current dates. PHP 8.2's security support ends Dec 31, 2026.
- Migrate to
PlatformArnfirst. It doesn't depend on PHP or Laravel, doesn't change the runtime, and makes every hop after it a one-line PR. - If your Laravel already supports the target PHP, do PHP next. It's a platform change with a quick rollback and a fixed deadline. AdSuite was on Laravel 12, moved to PHP 8.4, then went to Laravel 13.
- If it doesn't, move Laravel to the bridge on the PHP you already run. For an app on PHP 8.2 and Laravel 10 that means 10 to 11 to 12, then PHP 8.4, then Laravel 13. An app on PHP 8.1 has to reach PHP 8.2 before Laravel 11.
- Upgrade Laravel one major version at a time, each in its own PR, using the official upgrade guide for each hop.
One Laravel 13 gotcha: the skeleton defaults session serialization to json. Pin it to
php in the upgrade PR so the upgrade doesn't change behavior, then switch to json in its
own PR. JSON closes off object-deserialization attacks if APP_KEY leaks, but it logs every
user out once.
Environments with no IaC
An environment with no CloudFormation stack has nothing to drift from, so the EB-native update is the right tool:
aws elasticbeanstalk update-environment --region <REGION> --environment-name <ENVIRONMENT_NAME> \
--platform-arn "arn:aws:elasticbeanstalk:<REGION>::platform/PHP 8.4 running on 64bit Amazon Linux 2023/<VERSION>"
It was measured in a sandbox at about 2 minutes each way for PHP 8.2 to 8.4 and back, with the same environment id, CNAME and database. The order, verification and rollback above all apply. Bringing these environments under CDK is planned as part of the move to Kubernetes.
References
- AdSuite's upgrade PRs: platform bump, PHP 8.4, CI on 8.4, Laravel 13, JSON sessions.
- Updating your Elastic Beanstalk environment's platform version
- Elastic Beanstalk PHP platform history
- Laravel upgrade guides: 10 to 11, 11 to 12, 12 to 13
- ADR-0036: Optimal Web Application CI/CD Pipeline, for how
deploy-ebships artifacts.
Questions: Ben Giese, or Dakota or Fabian on the platform team.