Upgrade PHP on a VPS: Debian & Ubuntu Guide

Php upgrade

I've been managing VPS environments for over 15 years. And the one thing that never changes is how many production servers I inherit running PHP versions that should have been retired years ago.

Last year, I took over a client's server still running PHP 7.0, a version that hit end-of-life in 2019. It still worked. That's actually the problem.

The upgrade process isn't hard. But the cost of ignoring it is real, and it compounds quietly.

This guide covers upgrading PHP on a Debian or Ubuntu VPS all the way up to PHP 8.4 or 8.5, with notes on what changed, what to watch for, and a few things older guides get wrong.

Before You Upgrade: Know Your Codebase

This guide targets PHP 8.4 and 8.5. But not every app can make that jump in one shot, and running a blind upgrade on a legacy codebase is a reliable way to take a site down.

I've seen it plenty of times. A server running PHP 5.6 with a CodeIgniter 3 app that was built in 2014 and hasn't been touched since. Or a Drupal 7 install. Or a WordPress site with plugins that predate PHP 7.0. These apps work fine on the version they were built for. They don't always survive a major version jump. Be careful and know your code!

PHP 5.6 and 7.x are still running on thousands (if not millions) of production servers.

If your codebase is one of them, audit before you upgrade. A few hard stops to check for:

  • mysql_* functions: removed entirely in PHP 7.0. Any code using mysql_connect(), mysql_query(), or similar will throw a fatal error. Migrate to mysqli_* or PDO first.
  • ereg() and related POSIX regex functions: also removed in PHP 7.0. Replace with preg_* equivalents.
  • mcrypt as a hard dependency: gone from core in 7.2. The Sury package keeps it available, but if it's wired into your app tightly, plan the migration to OpenSSL.
  • Call-time pass-by-reference (foo(&$var)): removed in PHP 7.0.
  • Short open tags (<?) without the = behavior changed in 7.0, can break template-heavy codebases.

CodeIgniter 3 officially supports PHP 5.6 through 7.4, but in practice it runs mostly fine on PHP 8.2. The main friction points are deprecation notices that surface as errors in strict configurations, and a handful of compatibility issues depending on which CI3 version and libraries you're running. PHP 8.3 and above get shakier. If you're on CI3 and targeting 8.2, test thoroughly in staging but don't treat it as a hard blocker. You can update your composer dependency to pocketarc/codeigniter and continue to run CI3. 

Drupal 7 reached end-of-life in January 2025. It supports up to PHP 7.4 and will break on PHP 8.x. If you're still running Drupal 7, the PHP upgrade conversation is the same conversation as the Drupal migration.

The staged upgrade reality. Some servers need to go 5.6 to 7.4 first, verify everything runs, then 7.4 to 8.x in a second pass. That's not failure. That's the right approach for legacy apps. The commands in this guide work the same regardless of which version jump you're making. Just substitute the version numbers.

If your app is on a modern framework or a recent CMS version, you're probably fine to jump straight to 8.4 or 8.5. But check first. A staging environment saves you hours of production recovery.

Running Outdated PHP Is a Security Problem, Not Just a Performance One

Performance is the easy sell. PHP 8.x is dramatically faster than anything in the 7.x branch. But the more pressing reason to upgrade isn't speed. It's exposure.

Every PHP version has a defined support window: two years of active bug fixes, followed by one year of security-only patches. After that, nothing. No CVE responses. No patches. Any vulnerability discovered after end-of-life stays unpatched on your server permanently.

According to W3Techs, over 60% of publicly accessible PHP sites were running end-of-life versions as of early 2025, with PHP 7 alone powering nearly half of all PHP websites despite losing security support in November 2022. Sixty percent. That's not theoretical risk. That's a documented attack surface at massive scale.

Running an end-of-life PHP version in production isn't a technical debt issue. It's a liability issue. The question isn't if something will go wrong, it's when.

The attack vectors are real and well-documented: remote code execution via unpatched deserialization bugs, type juggling exploits, object injection in older extension versions. These aren't exotic CVEs sitting in academic papers. They're weaponized and actively scanned for.

PHP 7.4 hit end-of-life in November 2022. PHP 8.0 followed in November 2023. PHP 8.1 ended active support in 2024 with security-only patches through December 2025.

Still running any of those? You're already past the line, or close to it.

The Current PHP Support Landscape

Before touching anything, know where your server stands. Here's the picture as of early 2026:

VersionStatusEOL Date
PHP 5.6EOLDecember 2018
PHP 7.3EOLDecember 2021
PHP 7.4EOLNovember 2022
PHP 8.1EOLDecember 2025
PHP 8.2ActiveDecember 2026
PHP 8.3ActiveDecember 2027
PHP 8.4ActiveDecember 2028
PHP 8.5Current latestDecember 2027 (active)

PHP 8.4 is the right call for most existing applications. Battle-tested, widely supported across frameworks and CMS platforms, and it gives you the longest runway before you're doing this again.

PHP 8.5 makes sense for greenfield builds, but test your existing apps before upgrading blind. And if you're still on 8.1, it's EOL as of December 2025. Get off it.

Your CMS Has Requirements

CMS and framework minimum requirements have been moving forward, and some of them will force your hand whether you're ready or not.

WordPress

WordPress's official minimum is PHP 7.2, but that's a floor, not a recommendation. WordPress 6.8+ was built and tested against PHP 8.3 and 8.4. Running WordPress on 8.1 technically works. But you're leaving real performance gains behind and your EOL clock is already running.

Craft CMS

Craft 5 requires PHP 8.2 minimum. Still on Craft 4? The floor is PHP 8.0, but that version is coming up to end-of-life, so you need 8.2 or higher regardless of which Craft version you're running. If you're on Craft 4 running PHP 8.0, you have two upgrades to plan, not one. Craft 6 is currently in alpha and migrating to Laravel under the hood. Final PHP requirements haven't been published yet, but plan on 8.2 at minimum given Laravel's own floor.

CodeIgniter

CodeIgniter 4.7 raised its minimum to PHP 8.2 and added full PHP 8.5 compatibility. Running CI on PHP 8.5 requires 4.7.0 or higher. But the floor is 8.2, not 8.5.

Laravel

Laravel 11 requires PHP 8.2 minimum. Laravel 12, released early 2025, also requires 8.2 but is optimized for 8.3 and 8.4. Running a Laravel application below 8.2 means you can't upgrade the framework without upgrading PHP first. One problem creates another.

Before You Touch Anything: Backup!

This should go without saying. I've watched developers skip this step and spend hours recovering from it. SSH into your server and back up your current PHP configuration before running a single command:

cp -a /etc/php/8.x /home/your_username/php_8x_backup

Replace 8.x with your currently installed version. If you're running PHP-FPM with per-site pool configurations, those files won't survive the upgrade automatically. Document them. Copy them. You'll need to manually recreate them under the new version. This will backup your pools and php.ini, allowing a somewhat quick rollback, if needed. 

Add the Sury Repository

The ondrej/sury PPA is the standard source for current PHP packages on Debian and Ubuntu. Well-maintained, actively updated, and covers every PHP version you'd need.

On Debian/Ubuntu (from official docs):

apt update
apt install -y apt-transport-https ca-certificates curl

DISTRO=$(dpkg --status tzdata | grep Provides | cut -f2 -d'-')
if [ "${DISTRO}" == "forky" ]; then DISTRO="bookworm"; fi

curl -sSLo /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg

echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ ${DISTRO} main" > /etc/apt/sources.list.d/deb.sury.org.list

apt update

The DISTRO detection pulls your release codename from tzdata metadata rather than relying on lsb_release, which may not be installed on minimal server builds. The forky fallback maps Debian's testing branch to bookworm packages since a native forky repo isn't available yet.

Once the repository is added, check what's currently installed:

dpkg --get-selections | grep php

Note everything on that list. You'll want to mirror the same extensions on the new version, minus a few that have been removed or renamed (covered below).

Installing PHP 8.5

Here's a solid baseline install command for a typical Nginx + PHP-FPM stack targeting PHP 8.5. Adjust to match your extension list from the dpkg output above.

apt install php8.5-bcmath php8.5-cli php8.5-common php8.5-curl php8.5-dev \
  php8.5-fpm php8.5-gd php8.5-gmp php8.5-igbinary php8.5-imagick \
  php8.5-intl php8.5-maxminddb php8.5-mbstring php8.5-mcrypt \
  php8.5-memcached php8.5-msgpack php8.5-mysql php8.5-readline \
  php8.5-soap php8.5-tidy php8.5-xml php8.5-xsl php8.5-zip

A few notes on what's in this list and why:

  • bcmath: required by Laravel, WooCommerce, and anything doing arbitrary precision math. Not optional if you're running any of those.
  • dev: only needed if you're compiling PECL extensions manually. Safe to drop if you're not.
  • imagick: native ImageMagick bindings. Required for image processing in WordPress, Craft, and most thumbnail pipelines.
  • intl: required by Craft CMS, Symfony, and Laravel internals for internationalization support. Add it regardless of whether your app explicitly asks for it.
  • maxminddb: the native extension for MaxMind GeoIP2 databases, replacing the old geoip extension dropped in PHP 8.0. The geoip2 Composer package works too, but the native extension performs better.
  • mcrypt: removed from PHP core in 7.2, but it lives on as a PECL/Sury package and is still installable via the ondrej/php repo. If your codebase depends on it, it's available. Migrate off it when you can. OpenSSL covers every use case mcrypt handled and then some.
  • json: baked into PHP 8.0 core. No separate package needed or available.
  • opcache: statically compiled into PHP 8.5. No separate package needed. On by default.

Switching PHP-FPM Versions

Here's the thing older guides gloss over: restarting PHP won't automatically switch your active version. Stop the old one and start the new one explicitly.

service php8.3-fpm stop
service php8.5-fpm start

Then update your Nginx site configurations to point to the new socket:

fastcgi_pass unix:/run/php/php8.5-fpm.sock;

If you're running php pools, that above might look more like {username}_fpm.sock, so update as needed. More on pools below. 

Test and reload:

nginx -t && systemctl reload nginx

Verify with a quick phpinfo() call in a temporary file. Then delete it immediately. Don't leave phpinfo() files sitting on production servers. They expose your entire environment configuration to anyone who finds them.

Migrating Your PHP Configuration

Don't copy your old php.ini directly over the new one. PHP minor versions frequently change defaults or deprecate directives, and blindly overwriting will cause unexpected behavior. Compare them instead:

diff /etc/php/8.3/fpm/php.ini /etc/php/8.5/fpm/php.ini

Port your custom values manually. The settings worth double-checking every upgrade: memory_limit, upload_max_filesize, post_max_size, max_execution_time, and date.timezone.

Also review /etc/php/8.5/fpm/php-fpm.conf and your pool configs in /etc/php/8.5/fpm/pool.d/. If you had per-site pools configured, recreate them here. You will need to move your pools from your prior PHP install directory. 

A php.ini Starting Point for PHP 8.4 and 8.5

After porting your settings manually, here's a production-ready configuration worth using as a baseline.

Drop this into your /etc/php/8.5/fpm/php.ini and adjust the values to match your server's resources and use case.

Btw, you don't need to review your php.ini file line-by-line. You can simply append this to the bottom, as new values will override any prior values up the file. 

realpath_cache_size = 4M
realpath_cache_ttl = 300
max_execution_time = 180
memory_limit = 1G
html_errors = On
error_log = error_log
max_input_vars = 10000
post_max_size = 120M
upload_max_filesize = 120M
max_file_uploads = 50
default_socket_timeout = 90
date.timezone = America/Chicago
sendmail_path = '/usr/sbin/sendmail -t -i -fnoreply@yourdomain.com -Fnoreply@yourdomain.com'
mysqli.allow_local_infile = On
session.use_strict_mode = 1
session.cookie_secure = 1
session.gc_probability = 5
session.gc_divisor = 100
session.gc_maxlifetime = 7200
session.sid_length = 32
opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 1024
opcache.interned_strings_buffer = 256
opcache.max_accelerated_files = 30000
opcache.max_wasted_percentage = 5
opcache.use_cwd = 1
opcache.validate_timestamps = 1
opcache.revalidate_freq = 0
opcache.jit_buffer_size = 1G
opcache.jit = tracing

General performance. realpath_cache_size and realpath_cache_ttl reduce filesystem stat calls by caching resolved file paths. On servers running a CMS or framework with deep include chains, this has a measurable impact. max_accelerated_files = 30000 covers large codebases comfortably. WordPress with WooCommerce and a handful of plugins can easily hit 10,000+ cached scripts.

Memory and uploads. memory_limit, post_max_size, and upload_max_filesize should be set in proportion to each other. post_max_size must always be higher than upload_max_filesize or large uploads will fail silently. The values above suit a CMS environment handling media uploads. Scale down for smaller VPS instances.

Sessions. session.use_strict_mode and session.cookie_secure are the two most important security settings here. Strict mode rejects uninitialized session IDs, preventing session fixation attacks. Cookie secure requires HTTPS. Enable it without a valid certificate and sessions will break. gc_maxlifetime = 7200 gives sessions a two-hour lifetime, with a 5% probability of garbage collection firing on any given request.

OPcache. opcache.validate_timestamps = 1 with revalidate_freq = 0 tells PHP to check for file changes on every request. Right for development and staging. In a strict production environment where deploys are controlled, setting validate_timestamps = 0 eliminates those filesystem checks entirely and squeezes out additional performance, but you'll need to manually clear the cache on deploy. opcache.interned_strings_buffer = 256 is well within the limits PHP 8.4 raised. The old 4095MB cap was lifted, making larger values viable on modern servers.

JIT. opcache.jit = tracing is the recommended mode for web applications. It traces hot code paths across the full call stack and compiles them to native machine code at runtime. jit_buffer_size is what actually activates JIT. Without it set to a non-zero value, JIT won't run regardless of the opcache.jit setting. 1G is appropriate for a dedicated server with significant PHP workloads. Scale to 128M or 256M on lighter instances.

A php-fpm.conf Baseline

The global php-fpm.conf controls FPM's master process behavior. Most of it can stay at defaults.

Find these lines in /etc/php/8.5/fpm/php-fpm.conf and update their values. These cannot be appended to the bottom, like the php.ini file. 

log_level = warning
emergency_restart_threshold = 20
emergency_restart_interval = 1m
process_control_timeout = 10s
events.mechanism = epoll

Log level. Default is notice, which is noisy. warning keeps the log actionable without burying real problems in routine chatter.

Emergency restart. If 20 child processes crash within a minute, FPM restarts automatically. This protects against cascading failures from a corrupted opcode cache or bad deploy without requiring manual intervention.

Process control timeout. Gives child processes 10 seconds to respond to signals from the master before the master considers them unresponsive. The default of 0 means no timeout, which can leave zombie workers sitting indefinitely.

epoll. Linux's most efficient I/O event mechanism. The default is auto-detection, but setting it explicitly avoids any ambiguity on systems where multiple mechanisms are available.

A www.conf Pool Baseline

The pool config in /etc/php/8.5/fpm/pool.d/www.conf is where the real performance tuning lives. Append these to the bottom:

user = www-data
group = www-data
listen = /run/php/php8.5-fpm.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 25
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 500
request_terminate_timeout = 180s

If you're running multiple pools and configs, you can customize pools in this same directory, and I like to name them by website. So, I'll have a pool named: /etc/php/8.5/fpm/pool.d/zadroweb.com.pool.conf and so on, per site. 

Socket vs TCP. Unix sockets (/run/php/php8.5-fpm.sock) are faster than TCP for same-server communication. Nginx talks to PHP over the socket directly without going through the network stack.

pm = dynamic. The right choice for most web servers. static wastes memory by keeping all workers alive whether you need them or not. ondemand is too slow to spin up under traffic spikes. Dynamic keeps a pool of idle workers ready while staying within your memory ceiling.

Sizing pm.max_children. This is the most important value in the file, and the default of 5 is almost certainly wrong for your server. The rough formula: divide available RAM (after OS and other services) by your average PHP process size. Check your current process size with:

ps --no-headers -o "rss,cmd" -C php-fpm8.5 | awk '{ sum+=$1 } END { printf "%d MB\n", sum/NR/1024 }'

A CMS stack typically runs 50-100MB per process. On a 4GB VPS with 2GB available for PHP, that's 20-40 workers. The values above are a reasonable starting point for a mid-range server, but adjust based on your actual memory.

pm.max_requests = 500. Forces each worker to restart after 500 requests. This is your safety net against slow memory leaks in third-party extensions or poorly written plugins. It adds a small overhead but prevents the gradual memory creep that eventually takes a server down at 2am.

request_terminate_timeout = 180s. Hard kills any worker that's been running for 3 minutes. Matches max_execution_time in php.ini. Without this, a runaway script can hold a worker hostage indefinitely regardless of what php.ini says — some code paths bypass max_execution_time entirely.

Clean Up the Old Version

Once you've confirmed the new version is running cleanly and your sites are responding correctly, remove the old packages:

apt purge php8.3-common
apt autoclean && apt autoremove
rm -rf /etc/php/8.3/

Run the dpkg command again to confirm nothing is left behind. Occasionally a transitive package from an older version hangs around and needs a manual purge.

The Bottom Line

Running EOL PHP in production isn't a "we'll get to it" situation. It's an active liability with no upstream coverage. The upgrade path on Debian and Ubuntu is well-worn at this point. The Sury repository handles package management cleanly, and the main friction is always application compatibility testing, not the upgrade mechanics themselves.

Test in staging. Review your extension list. Port your ini settings manually. Then make the switch.

If you'd rather hand this off entirely, our managed hosting keeps your stack current as a standard part of the service, not an upsell.

Have questions or ran into something this guide didn't cover? Reach out.

Browse More in Cloud Computing

Explore insights about cloud computing with our informative articles. From infrastructure to information technology, security, migrations, and server builds, we cover trending topics on the many benefits of the cloud. Stay ahead of the curve in this continually evolving technology landscape.

Http3 introduction

HTTP/3: An Introduction to the Next Generation Web Protocol

Move over HTTP/2; there's a new kid on the block.HTTP/3 is finally here, and everybody's talking about it. Well, it's actually…

Lemp tutorial digital ocean

Debian Install with Nginx and WordPress on Digital Ocean

I love VPS servers. Configuring them and customizing cloud solutions brings me joy.For the last seven years or so, we have…

Digital ocean create droplet

Quickstart LAMP Setup Guide - Digital Ocean

It's no secret that most shared hosting platforms are just terrible. This includes HostGator, BlueHost, and any other you can…