MOOBON
Technical Article · AWS / WordPress

Migrating 14 Sites and 28 WordPress Installs from Amazon Linux 2 to 2023Every landmine we hit going from PHP 5.6 to 8.5

Published May 1, 202620 min readMOOBON Tech Blog

Standard support for Amazon Linux 2 ends on June 30, 2026. Servers still running WordPress on PHP 5.6 are hardly rare out in the field. This article is a first-hand account of a large-scale migration of a production server hosting 14 sites and 28 WordPress installs from AL2 to AL2023, and from PHP 5.6 to 8.5.

Should you rebuild on AL2023 yourself, or get out of server management altogether? ── To help you decide, we share every pitfall we ran into and how we dealt with it.

Introduction: why we are writing this now

The end of standard support for Amazon Linux 2 (June 30, 2026) is right around the corner. The longer a WordPress environment has been running in production, the more likely it is that its PHP, MySQL, and Apache versions have been left untouched.

This article records the full set of work involved in migrating one production server (hosting 14 sites and 28 WordPress installs) from Amazon Linux 2 to 2023, PHP 5.6 to 8.5, MySQL 5.7 to MariaDB 10.11, and Apache to nginx. We leaned heavily on an AI assistant, and the hands-on work took roughly one day. That said, we are sharing everything, including the parts riddled with landmines that could not simply be left to the AI.

If you are in the same situation, we hope this helps you decide whether to (1) migrate on your own, (2) outsource the migration, or (3) step away from server management altogether.

1. Before and after: stack comparison

The old server was a 2019 build that had been kept alive ever since, running 26 WordPress databases and 28 installs on PHP 5.6 and MySQL 5.7.

ItemOld server (AL2)New server (AL2023)
OSAmazon Linux 2 (kernel 4.14, 2019 build)Amazon Linux 2023 (kernel 6.1)
WebApache 2.4 + mod_pagespeednginx 1.28
PHP5.6.408.5.4
DBMySQL 5.7.28MariaDB 10.11.15
Storage100GB gp2 (78GB used)30GB gp3, encrypted (24GB used)
WordPress28 installs / 26 DBs / mix of core 3.x to 6.xAll updated to WP 6.9.4

Besides WordPress, the server carried legacy assets such as custom PHP code, Welcart EC, BuddyPress, bbpress, and the Luxeritas theme, so we knew going in that the PHP version jump would be the hardest part.

2. Migration strategy: running in parallel

We chose a "parallel-run" approach: leave the old server as is, build a new AL2023 instance alongside it, verify, then switch DNS. An in-place upgrade to Amazon Linux 2023 isn't possible, and since this involved not just the OS but a four-step PHP major version jump, a setup that let us verify while production kept running was essential.

# create AWS resources (with a named profile)
aws ec2 create-security-group --group-name <prefix>-al2023 ...
aws iam create-role --role-name <prefix>-al2023-role ...
aws ec2 run-instances --image-id ami-XXXXXXXX --instance-type t3.medium ...
aws ec2 allocate-address --domain vpc ...

The configuration we chose was t3.medium (with an RI), 30GB encrypted gp3, and an IAM Role with S3 write permissions. How we got from 100GB on the old server down to 30GB is covered in the next section.

3. Data migration: shrinking 63GB to 23GB with rsync

The old server's public directory was 63GB, but by excluding the clearly unnecessary items below we brought it down to 23GB, which meant the new server could run on a 30GB gp3 volume.

  • Assorted backups (ai1wm-backups/, backwpup-*/, *.wpress)
  • Mail directories (handled separately)
  • Old .exe / .db files under an unused dl/ directory (about 30GB)
  • The contents of subdomains that had been dormant for a long time

The transfer ran over private IPs inside the VPC and finished in about 5 minutes.

rsync -aHA --numeric-ids \
  --exclude="ai1wm-backups/" \
  --exclude="backwpup-*/" \
  --exclude="*.wpress" \
  --exclude="/<domain>/public_html/dl/" \
  --exclude="/<old-subdomain>/" \
  ec2-user@10.0.1.176:/srv/sites/ /srv/sites/

For MySQL, we dumped all 26 databases in one go with mysqldump --add-drop-database --routines --triggers --events and imported them into MariaDB on the new server. MySQL 5.7 → MariaDB 10.11 is mostly straightforward, but DEFINER clauses and the change in default charset between utf8mb3/utf8mb4 need individual attention.

The main event

4. PHP 5.6 → 8.5 compatibility: every hellish pattern

With a four-step major version jump (5.6 → 7.0 → 7.4 → 8.0 → 8.5), we ran into a huge number of incompatibilities. WordPress core could be updated to 6.9.4 in one pass with wp-cli core update, but the incompatibilities in themes and plugins that hadn't been updated in years meant one manual fix after another.

PatternPHP behaviorFixScope
$str{0} (string offset with curly braces)Deprecated in PHP 7.4 / removed in 8.0Replace with $str[0]Dozens of files
=& new ClassName() (assign by reference)Deprecated in PHP 5.3 / removed in 7.0Change to = new ClassName()52 files
create_function('', 'code')Deprecated in PHP 7.2 / removed in 8.0Convert to a closure: function() { code }253+ files
Signature mismatch in Walker subclassesStrict in PHP 8.0Add default values to make it compatible with the parentMany
Non-static methods called staticallyFatal in PHP 8.0Change to static functionPer plugin
Smart quotes (' ' " " ) used in string literalsFatal in PHP 8.0Replace with ASCII quotes (across the whole theme)Entire theme
Collision with class Match (reserved word in PHP 8)Became a reserved word in PHP 8.0Rename to something like class SR_Match1 plugin
ksort($wp_filter[$tag]) (WP_Hook object)TypeError in PHP 8.0Rewrite as ksort($wp_filter[$tag]->callbacks)bbpress / BuddyPress
break 2 (wrong loop depth)Fatal in PHP 8.0Change to break;1 plugin
Property access on nullWarning → fatal in PHP 8.0Add null checksMany themes
Dynamic properties deprecatedDeprecated in PHP 8.2Add the #[\AllowDynamicProperties] attributeCore & many others

Case ①: BuddyPress and the $wp_admin_nav array problem

Up through PHP 7, pushing onto an uninitialized variable as if it were an array worked without error, but in PHP 8 the type mismatch throws a TypeError.

// BEFORE — PHP 7 carries on even when array-pushing onto an empty string ""
$wp_admin_nav[] = array('parent' => ...);

// AFTER — PHP 8 throws a TypeError, explicit initialization is required
if (!is_array($wp_admin_nav)) $wp_admin_nav = array();
$wp_admin_nav[] = array('parent' => ...);

Case ②: removing create_function() from 253 files

The most frequent incompatibility on this project was replacing create_function(), which was removed in PHP 8.0. We wrote sed/perl patterns that parse the arguments and replace each call with a closure, then followed a mechanical replace → verify routine.

Case ③: WordPress 3.x sites won't boot because of mysql_*

Some sites had been left on WP 3.x, and since the mysql_* functions don't exist in PHP 8, WordPress itself won't even start. You must first bring core up to date with wp-cli core download --force and only then work on getting everything consistent.

Lesson learned

PHP 8 is strict, period. Things that were warnings in PHP 7 have been promoted across the board to fatal errors. Many old WP plugins haven't been updated by their authors in ages, so the work comes down to deciding, one by one, among three options: patch it yourself / replace it with another plugin / just disable it.

5. nginx vhost design: converting from Apache mod_rewrite

The old Apache setup used DocumentRoot /wp plus a fallback rewrite to route requests to sub-WordPress installs such as /public_html/cart. Reproducing that exactly in nginx is difficult, so we switched to leveraging the existing symlink structure on the filesystem.

# domain A (DocumentRoot is /wp, sub-WP installs reached via symlinks)
server {
    server_name <domain-a.example> www.<domain-a.example> pro.<domain-a.example>;
    root /srv/sites/<domain-a.example>/public_html/wp;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php-fpm/www.sock;
        # ... fastcgi_params
    }
}

Because the parent domain and subdomains shared web fonts, we add a CORS header for fonts only.

location ~* \.(woff2?|ttf|eot|otf)$ {
    add_header Access-Control-Allow-Origin "*";
}

6. Minimizing EBS and designing the ops foundation: AMI backups + AWS cost estimate

Our priority in this migration was to keep EBS as small as possible and minimize monthly running costs. From 78GB in use on the old server, we got the new server down to 24GB. By leaving backups to AWS AMI snapshots and immediately offloading access logs and DB dumps to S3, we broke away from a setup where data keeps piling up on EBS.

Design principles

  1. Consolidate backups on AMI snapshots: remove every WordPress backup plugin (All-in-One WP Migration, BackWPup, UpdraftPlus, etc.) and switch to capturing the whole OS with AWS AMIs.
  2. Offload access logs to S3 immediately: copy them to S3 within logrotate and keep only 7 days locally on EBS.
  3. Daily DB dumps to S3: mysqldump → S3, with minimal local retention.
  4. Clear out years of accumulated backups: delete every legacy .wpress / .tar / .htaccess_yyyymmdd file (see Section 8).

logrotate → S3

/var/log/nginx/sites/*.log {
    daily
    rotate 7
    lastaction
        DATE_DIR=$(date -d yesterday +%Y/%m/%d)
        for f in /var/log/nginx/sites/*.log.1.gz; do
            aws s3 cp "$f" \
              "s3://<bucket>/logs/nginx/sites/$DATE_DIR/$(basename $f .log.1.gz).log.gz"
        done
    endscript
}

An S3 lifecycle rule expires everything under the logs/ prefix after 90 days, keeping storage costs capped as well.

DB backups → S3 (daily at 5:00)

# /etc/cron.d/db-backup
0 5 * * * root /usr/local/bin/db-backup.sh

Even uploading all 26 databases every day comes to only about 35MB in total. Combine it with lifecycle transitions to S3 Glacier Instant Retrieval or IA and long-term storage costs drop even further.

Note: Amazon Linux 2023 does not ship with cronie (the cron daemon) by default. We deliberately installed cronie for this setup, but rewriting these as systemd timers is more in line with the AL2023 way of doing things.

Why consolidate on AMI backups

Plugin-based backups are convenient, but they pile up compressed archives (.wpress / .zip / .tar) under wp-content indefinitely, forcing EBS to carry capacity it doesn't really need. This is especially serious on servers hosting multiple sites on a single machine; on this project, leftovers from backup plugins alone accounted for tens of gigabytes.

On the new server, backups are consolidated on AWS AMI snapshots (billed incrementally). Every WordPress backup plugin was removed, and recovery is designed around rolling back the entire instance.

AWS cost estimate: keeping 10 generations of AMI snapshots

Assuming we keep 10 generations of AMI snapshots, here is a comparison of EBS-related costs for the old and new setups (calculated with typical Tokyo region pricing).

ItemOld server (AL2)New server (AL2023)Difference
EBS volume
($0.12/GB gp2 / $0.096/GB gp3)
100GB gp2
= $12.00 / month
30GB gp3
= $2.88 / month
−$9.12
AMI snapshots
× 10 generations
($0.05/GB, billed incrementally.
Estimated at ~1.2× the initial size)
78GB × 1.2 ≈ 94GB
= $4.68 / month
24GB × 1.2 ≈ 29GB
= $1.44 / month
−$3.24
Total~$16.68 / month~$4.32 / month−$12.36 / month
(~74% less)

That is a saving of roughly $12 / ¥1,800 per month (about ¥22,000 a year at ¥150/USD). The amount itself isn't large, but it adds up when you run multiple servers, and you also get an operational benefit: the smaller the EBS volume, the faster AMI creation and restores become.

Assumptions
  • Calculated with typical Tokyo region (ap-northeast-1) pricing. Actual prices vary by region and over time.
  • EBS snapshots are billed incrementally. The total for 10 generations is approximated as "initial full size × ~1.2" (assuming a limited rate of data change, since logs and DB dumps are sent to S3). Real-world figures vary with the type of business and update frequency.
  • gp3's baseline of 3,000 IOPS / 125 MB/s is included at no extra charge, so no additional IOPS charges are added.
  • The EC2 instance itself, data transfer, and S3 storage are calculated separately.

7. Security hardening

Years of operation had opened up more and more holes on the old server, so we used the migration to tighten everything up in one go. Before the audit, 12 world-writable directories and 404 world-writable files remained; all of them have been fixed.

CategoryMeasure
Sensitive filesReturn 403 from nginx for wp-config.php, xmlrpc.php, install.php, readme.html
Block PHP execution via uploadslocation ~* /wp-content/uploads/.*\.(php|phtml)$ { deny all; }
User enumeration protection403 for ?author=N, 403 for /wp-json/wp/v2/users
HTTP security headersX-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, HSTS
Hide version informationserver_tokens off, expose_php = Off
Consistent file permissionsDirectories 755 / files 644 / wp-config.php 640
Disable file editing from WP adminAdded define('DISALLOW_FILE_EDIT', true) to all 26 wp-config files

8. Cleanup: 690MB removed

While we were at it, we cleared out years of accumulated cruft.

CategoryRemoved
Unused plugins (accumulated from the old server era)98
Unused themes (old twentyXX themes, etc.)86
Backup plugins (All-in-One WP Migration, BackWPup, UpdraftPlus)14
Backup files (.wpress, .tar, .htaccess_2019xxxx, etc.)About 400 files
Total reductionAbout 690MB

9. Outbound mail: Postfix + SES SMTP relay

The old server sent mail directly through Postfix (no relayhost configured). On the new server we keep Postfix but point its SMTP relay at AWS SES.

[26 WordPress sites] → PHP mail() → Postfix → SES SMTP → Recipient
  • No need to install the WP Mail SMTP plugin on 26 sites
  • No changes needed to existing Contact Form 7 / mw-wp-form setups
  • Better deliverability with automatic SPF / DKIM signing
  • Low cost at $0.10 per 1,000 emails

10. Fixes during client verification

During the verification period, we received many requests to fix broken layouts and small UI details. Typical ones included:

  • Replacing or removing defunct third-party widgets (ws-fe.amazon-adsystem.com, Pocket, etc.)
  • In WordPress 6.7+, sizes="auto" caused thumbnails to render unexpectedly huge → disabled via an mu-plugin
  • Rebuilding the theme's post list layout (image left / text right) with CSS Grid
  • Fixing places where the WordPress version leaked via <meta name="generator">
  • Making a custom JS null.id error null-safe

11. Remaining tasks

  • Move to ALB + ACM (production HTTPS): switch over once the client signs off on verification
  • DNS cutover (old → new): same as above
  • DNS settings for AWS SES such as DKIM: waiting on the client to update their DNS
  • Restrict source IPs allowed for SSH/HTTPS in the Security Group: at production release

12. Effort comparison: how to think about the cost

With the help of an AI assistant, we completed the migration of 28 sites in about one day of hands-on work (8 to 10 hours). Doing the same volume without AI, the PHP 8 compatibility fixes alone could easily have taken several days to a week.

OptionUpfront costMonthly running costOperational effort
Migrate to AL2023 yourselfSeveral days to several weeks of effort
(depending on expertise)
EC2 / EBS / backup fees + in-house operating costsPatching / monitoring / incident response all in-house
Have MOOBON handle the AL2023 migrationFrom ¥100,000 per site as a guideline
(quoted separately depending on scale)
EC2 / EBS / backup feesOperations stay in-house
(a maintenance contract is available as a separate option)
Move to Kinsta (managed WP)Migration: free migration by Kinsta or support from MOOBONFrom $35/month
(depending on plan)
Patching, monitoring, backups, and CDN all handled by Kinsta

Using the Amazon Linux 2 EOL as the moment to "get out of server management altogether" is a rational choice, especially for companies that run WordPress as one part of their business. As Japan's first official Kinsta partner, MOOBON provides one-stop support from migration through ongoing operations.

13. Advice for anyone about to migrate

  1. Estimate the PHP 8 compatibility of old WordPress installs and plugins up front with static analysis. Run Rector and PHPCS with the PHPCompatibility ruleset in CI and you will know the scope of impact on day one.
  2. Settle your rsync exclusions before you migrate. Having a transfer stop midway because EBS filled up is a pain. We got from 63GB down to 23GB because we took the time to design the exclusion strategy.
  3. Handle environments with many WP installs using automation scripts that drive WP-CLI. Manually editing 28 wp-config.php files is a breeding ground for mistakes.
  4. Use an SES relay for outbound mail from the start. Building it in when you set up the new server is far easier than retrofitting the Postfix configuration later.
  5. Set aside a separate period for client verification. Even if everything is technically near-perfect, you will always get feedback on small visual details.
  6. Seriously consider not migrating at all and escaping to managed hosting (Kinsta, etc.). It is worth comparing the cost of tackling each of 28 sites' individual quirks (plugin compatibility, SSL, mail, cron) one by one against a monthly fee plus migration costs.
Endnote

Closing thoughts

We hope this article is useful to anyone else working through the Amazon Linux 2 EOL. Big version jumps from PHP 5.x and migrations of long-running WordPress environments are an area with plenty of pitfalls but little consolidated information, so we would be glad if this serves as input for decisions in the field.

Depending on the size of your sites and your budget, there is also the option of choosing a managed service like Kinsta instead of continuing to manage the OS yourself, freeing you from OS management entirely.

If you would like to discuss anything related to this, please feel free to get in touch.

FAQ

Frequently Asked Questions

QWhen does Amazon Linux 2 reach end of support?
A

Standard support for Amazon Linux 2 ends on June 30, 2026. After that, no more security patches will be released, so you need to migrate to Amazon Linux 2023 (AL2023). For new builds, choose AL2023 from the start; for existing environments, we recommend planning the migration well ahead of EOL (ideally six months or more in advance).

QWhat was the biggest landmine in moving from PHP 5.6 to PHP 8?
A

The removal of create_function() had by far the widest impact: in the environment covered here, it was used in 253 files. Every one of them has to be rewritten as an anonymous function (closure). Runners-up include string offsets with curly braces ($str{0}), assign by reference (=& new), Walker class signature mismatches, and deprecated dynamic properties (PHP 8.2). The longer a WordPress environment has been running, the more old-style code lingers inside its themes and plugins, so an efficient first step is to grep the entire codebase and build a list.

QHow should I rewrite create_function?
A

It was deprecated in PHP 7.2 and removed in PHP 8.0. The basic pattern is to replace create_function('$x', 'return $x * 2;') with function ($x) { return $x * 2; }. If code was being generated fully dynamically via eval, some cases call for a design rethink (splitting it into config files or a strategy pattern). The realistic approach is to bulk-convert with an automated tool such as Rector and handle the remaining hard cases by hand.

QCan an AL2 → AL2023 migration be done with zero downtime?
A

With a parallel-run approach, you can cut over with effectively only a few minutes of downtime. In this article we built the new environment on AL2023 + PHP 8.5 → synced the WordPress core, uploads, and DB dumps with rsync → verified behavior → switched DNS. If you need true zero downtime, you can set up weighted traffic shifting (Blue/Green) behind an ALB, weighing it against the cost of managing write consistency.

QHow do I check whether my WordPress plugins are PHP 8 compatible?
A

The reliable way is to bring up a staging environment on PHP 8 before going to production and exercise every screen, every feature, and every cron job. You can check a plugin author's update status via the "Tested up to" PHP version on WordPress.org or the changelog, but if there is no recent release, grep the source code directly for incompatible code such as create_function or each(). Many cases can be solved by switching to an alternative plugin, so we also recommend taking inventory of your plugins as part of the migration.

MOOBONISO/IEC 27001 CertificationIT Introduction Subsidy Support ProviderAWS Partner Select Tier Services
Copyright © 2026 MOOBON, Inc. All Rights Reserved.
Standard: ISO/IEC 27001:2022
Scope: Web system design support / Development, operation, and maintenance of in-house cloud services / Contract system development, operation, and maintenance / Server construction, operation, and maintenance