Authenticating...
Skip to main content

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.

  1. 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.
  2. Bump PHP, keep the platform version. For example, PHP 8.2 to PHP 8.4, both on 4.13.5. cdk diff should show only PlatformArn changing, with no replacement.
  3. Build on the new PHP in CI. Bump PHP_VERSION in 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 your Dockerfile builds FROM a :latest base, Docker reuses a cached copy and developers keep running the old PHP. Set pull: true under the Compose build: key, or rebuild with docker compose build --pull.
  4. Raise the floor. Bump require.php in composer.json, drop any config.platform.php pin, and update the docs last. While composer.json still allows the old version, a rollback stays open.

Before step 2:

  1. 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.
  2. Find packages that cap the PHP version with composer why-not php 8.4.
  3. Update the capped packages on the old PHP. Run composer update with Composer on the version production runs today, or pin config.platform.php to 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.
  4. Read .platform/hooks for 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:

  1. describe-environments reports the new PlatformArn.

  2. The instance AMI name contains the new branch, for example eb_php84.

    The first two work with the read-only role. The php checks need SSM access.

  3. php -v over SSM on an instance of each tier reports the new version.

  4. php -m over SSM, diffed against a baseline taken before the deploy, shows only the modules you expect to change. That's how pspell dropping out of 8.4 was caught.

  5. The health check and a real request return 200.

  6. The Datadog tracer is loaded. It's the most likely thing a platform hook breaks.

  7. Queue workers (Horizon) and the scheduler are running.

  8. 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​

  1. 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.
  2. The nginx version isn't proof of a PHP change. It moves with the platform version, not the PHP branch.
  3. Code that only works because of dev dependencies. A config file that references a require-dev package is harmless while vendor/ ships whole and fatal if anything switches to composer install --no-dev.
  4. Platform hooks hardcode things. Secret names, extension installers and PHP paths in .platform/hooks can 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.

LaravelSupported PHP
108.1 to 8.3
118.2 to 8.4
128.2 to 8.5
138.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.

  1. Migrate to PlatformArn first. It doesn't depend on PHP or Laravel, doesn't change the runtime, and makes every hop after it a one-line PR.
  2. 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.
  3. 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.
  4. 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​


Questions: Ben Giese, or Dakota or Fabian on the platform team.