Debian 13 ships PHP 8.4 in its own archive, while Debian 12 stops at PHP 8.2 and needs the Sury repository for 8.4. Both sources already carry the fix for PHP2Shell, the pair of remote code execution bugs patched upstream on 21 October 2026, so a server built from this page starts out patched. An older 8.4 install needs the upgrade and the restart shown in the check section, because the fix only counts once the running PHP processes are new.
Below we install PHP 8.4 with the extensions most applications need, tune php.ini and the PHP-FPM pool, check the build against PHP2Shell, connect Nginx or Apache, and run a second PHP version beside it. Every install and configuration step here was run in October 2026 on fresh Debian 13 and Debian 12 systems, plus an upgrade of an unpatched Debian 13 server, and the output blocks come from those runs.
Prerequisites
Before you start, make sure you have the following in place:
- A server running Debian 13 (Trixie) or Debian 12 (Bookworm)
- Root access or a user with sudo privileges
- A working internet connection
- Basic familiarity with the Linux command line
Start by updating your system packages, because the PHP builds link against current Debian libraries and a stale system is where dependency errors come from:
sudo apt update && sudo apt upgrade -y
On a server that already has PHP 8.4 from Debian 13’s archive, that upgrade also pulls the patched packages from trixie-security, where the PHP2Shell fix lives. On a fresh server there is no PHP to upgrade yet, so the patched build arrives with the install in Step 2.
Step 1: Add the Sury PHP Repository
Debian 12 needs this step, Debian 13 does not. Bookworm’s archive has no PHP 8.4 at all, so on Debian 12 the packages come from Sury, the repository run by OndΕej SurΓ½, who also maintains PHP in Debian itself. It publishes every stable PHP branch from 5.6 to 8.5, plus an 8.6 release candidate whose 8.6.0~rc3 build predates the PHP2Shell fix. Trixie carries php8.4 in its own archive, so on Debian 13 skip to Step 2 unless you also want a second PHP version (Step 8). Adding Sury on Debian 13 has a side effect you should know about: its package versions sort higher than Debian’s, so on our Debian 13 test system the next apt update listed 17 installed PHP packages as upgradable to Sury’s builds of the same PHP 8.4.27.
Debian 11 and Debian 10 are out of Debian support. Sury still publishes patched PHP 8.4 builds for Debian 11 (bullseye), while Debian’s own PHP 7.4 there gets the PHP2Shell fix only through Freexian’s paid ELTS. Debian 10 (buster) has no Sury repository any more, so a buster box first needs the Debian 10 to 11 upgrade.
Upgrade those servers a release at a time, because Debian supports upgrades only from the previous release; the last hop is in the Debian 12 to 13 upgrade guide.
The older version of this step installed software-properties-common and gnupg2. On Debian 13 that line now stops with Error: Unable to locate package software-properties-common, because trixie dropped the package, and neither is needed. Install the three tools the setup really uses:
sudo apt install -y lsb-release ca-certificates curl
Sury publishes its signing key as a small Debian package. It drops the keyring into /usr/share/keyrings/ and keeps it current when the key rotates, so you never manage key files by hand:
curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
sudo dpkg -i /tmp/debsuryorg-archive-keyring.deb
Add the repository with the same keyring path and file name as Sury’s own README. lsb_release -sc fills in your release codename:
echo "deb [signed-by=/usr/share/keyrings/debsuryorg-archive-keyring.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/php.list
Update the package index to pull in packages from the new repository:
sudo apt update
Look for the two packages.sury.org lines, the release file and the package list. A signature error here means the keyring package did not install, so rerun the dpkg -i line:
Hit:1 http://deb.debian.org/debian bookworm InRelease
Hit:2 http://deb.debian.org/debian bookworm-updates InRelease
Hit:3 http://deb.debian.org/debian-security bookworm-security InRelease
Get:4 https://packages.sury.org/php bookworm InRelease [6136 B]
Get:5 https://packages.sury.org/php bookworm/main amd64 Packages [290 kB]
Fetched 297 kB in 0s (814 kB/s)
Confirm that apt now sees PHP 8.4 and where it comes from:
apt-cache policy php8.4
On Debian 12 the only candidate is Sury’s build, and the version string carries the 21 October 2026 snapshot date of the patched release:
php8.4:
Installed: (none)
Candidate: 8.4.27-1+0~20261021.57+debian12~1.gbp7636b9
Version table:
8.4.27-1+0~20261021.57+debian12~1.gbp7636b9 500
500 https://packages.sury.org/php bookworm/main amd64 Packages
With that candidate in place, Debian 12 installs the patched build in the next step without any version pin.
Step 2: Install PHP 8.4 and Common Extensions
The same line works on both releases. Keep php8.4-fpm in it: the php8.4 metapackage needs a web server SAPI, and with FPM on the same line apt is satisfied. On its own, php8.4 picks the Apache module instead, and a dry run on our Debian 12 system wanted to install apache2 and libapache2-mod-php8.4, which then sits on port 80 before you have chosen a web server.
sudo apt install -y php8.4 php8.4-cli php8.4-fpm php8.4-mysql php8.4-pgsql php8.4-curl php8.4-gd php8.4-mbstring php8.4-xml php8.4-zip php8.4-intl php8.4-opcache php8.4-redis
The extensions on that line cover most database-backed applications:
- cli: Command-line interface for running PHP scripts from the terminal
- fpm: FastCGI Process Manager for serving PHP with Nginx or Apache
- mysql: MySQL and MariaDB database support
- pgsql: PostgreSQL database support
- curl: HTTP client library for making API requests
- gd: Image processing library
- mbstring: Multibyte string handling for UTF-8 and other encodings
- xml: XML parsing and manipulation
- zip: ZIP archive support
- intl: Internationalization functions
- opcache: Bytecode caching for better performance
- redis: Redis key-value store support for sessions and caching
The mysql extension talks to MariaDB as well as MySQL, so if the database lives on the same server, the MariaDB install guide for Debian covers the server side.
Verify the Installation
Check that PHP 8.4 is installed and working:
php -v
Debian 13 printed this. Sury’s build on Debian 12 shows the same version with a different build time:
PHP 8.4.27 (cli) (built: Oct 21 2026 07:53:46) (NTS)
Copyright (c) The PHP Group
Built by Debian
Zend Engine v4.4.27, Copyright (c) Zend Technologies
with Zend OPcache v8.4.27, Copyright (c), by Zend Technologies
List all installed PHP modules to confirm the extensions are loaded:
php -m
Each extension from the install line shows up under its module name, for example gd, intl, pdo_pgsql and redis. A missing one is covered in Troubleshooting below.
Check PHP 8.4 for PHP2Shell (CVE-2026-109618 and CVE-2026-109619)
PHP2Shell is two critical remote code execution bugs in PHP itself, CVE-2026-109618 and CVE-2026-109619, which php.net scored CVSS v4.0 9.2 in its 20 October pre-announcement. Upstream fixed both on 21 October 2026 in PHP 8.4.27, alongside 8.5.12, 8.3.36 and 8.2.35, and both sources in this guide ship the fixed 8.4.27: Debian 13’s 8.4.27-1~deb13u1 from trixie-security (DSA-6574-1) and Sury’s build on Debian 12. Details are in the PHP security advisory.
The package changelog is the reliable check, because both packagers name the CVEs in the entry that fixes them:
zgrep -E 'CVE-2026-10961[89]' /usr/share/doc/php8.4-common/changelog.Debian.gz
Debian 13 and Sury on Debian 12 printed the same line. No output at all means the installed build is older than the fixed one:
+ (CVE-2026-109618, CVE-2026-109619): Avoid state corruption via
Minimal and container images often strip /usr/share/doc, so the changelog may not be there to read. This check compares the installed package version with the fixed one and needs no documentation files:
dpkg --compare-versions "$(dpkg-query -W -f='${Version}' php8.4-common)" ge 8.4.27 && echo patched || echo VULNERABLE
Both fresh installs printed patched. Our Debian 13 server still on 8.4.24-1~deb13u1 printed the other answer:
VULNERABLE
A fresh install from this guide can stop here and carry on with Step 3. On an existing server that came back vulnerable, upgrade every PHP 8.4 package. The Apache module and the embed library start with lib, so the glob misses them and they are named explicitly; apt skips whichever of them are not installed. Our unpatched Debian 13 test server had six PHP 8.4 packages, and all six moved to 8.4.27:
sudo apt update
sudo apt install -y --only-upgrade 'php8.4*' libapache2-mod-php8.4 libphp8.4-embed
Then restart the PHP handler the server actually uses. If php8.4-fpm is installed, restart it. The package restarted it by itself in our Debian 13 test, but don’t rely on that; the restart is how you know the old workers are gone:
sudo systemctl restart php8.4-fpm
For mod_php (libapache2-mod-php8.4), restart apache2 instead, and if both are installed, run both restarts. Skip whichever one you don’t have: on our FPM and Nginx server, systemctl restart apache2 only answered Unit apache2.service not found. Long-running PHP CLI workers, such as queue consumers under supervisor or their own systemd unit, keep the old code in memory too, so restart those services as well:
sudo systemctl restart apache2
Now prove the running service is newer than the package on disk. This prints the last time PHP 8.4 was installed or upgraded, rotated logs included:
zgrep -hE ' (install|upgrade) php8.4-(common|fpm):' /var/log/dpkg.log* | sort | tail -1
On the Debian 13 test server it shows the move from 8.4.24 to 8.4.27:
2026-10-22 20:44:48 upgrade php8.4-fpm:amd64 8.4.24-1~deb13u1 8.4.27-1~deb13u1
Compare it with the moment the restarted service came up. On a mod_php server, ask about apache2 instead:
systemctl show -p ActiveEnterTimestamp php8.4-fpm
The timestamp has to be later than the dpkg line, as it is here:
ActiveEnterTimestamp=Thu 2026-10-22 20:45:05 UTC
Finally, ask the web server itself, because php -v starts a new process and always shows the package on disk. Drop a one-line probe into the site’s document root (/var/www/example.com in this guide, /var/www/html on a default site):
echo '<?php echo PHP_VERSION, " ", PHP_SAPI, "\n";' | sudo tee /var/www/example.com/v.php
Request it by the site name, the same way Step 7 does before DNS exists:
curl -H "Host: example.com" http://127.0.0.1/v.php
A patched FPM site answers with the fixed version and the fpm-fcgi SAPI, as our upgraded Debian 13 server and the Debian 12 Sury install both did. A mod_php site shows apache2handler instead, and any older version means a worker was not restarted:
8.4.27 fpm-fcgi
Delete the probe once you have the answer:
sudo rm /var/www/example.com/v.php
A fresh install has nothing in that directory until Step 5 or 6, so it can run the probe after Step 7 instead.
Step 3: Configure php.ini Settings
The default php.ini values are conservative and usually need adjusting for production workloads. There are separate php.ini files for CLI and FPM. We’ll focus on the FPM config since that’s what serves your web applications.
Open the FPM php.ini file:
sudo vim /etc/php/8.4/fpm/php.ini
Find and update these settings. These values work well for most web applications, so adjust based on your specific needs:
General Settings
Memory, upload size, timeouts and the time zone sit in the core section of the file. Keep post_max_size at least as large as upload_max_filesize, because a POST body larger than post_max_size is dropped before the upload limit is even checked:
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
max_input_time = 300
max_input_vars = 5000
date.timezone = UTC
Set date.timezone to your own zone, such as Africa/Nairobi, if your application logs or schedules jobs in local time.
OPcache Settings
OPcache stores precompiled PHP bytecode in shared memory, which eliminates the need to parse scripts on every request. This is one of the single biggest performance wins you can get.
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 2
opcache.save_comments = 1
opcache.validate_timestamps = 1
A quick note on opcache.validate_timestamps: set this to 0 in production if you deploy with a clear opcache step. When set to 0, PHP won’t check if files have changed, which gives you a small performance bump. But if you edit files directly on the server, keep it at 1.
Verify the configuration changes with the FPM binary, not plain php. The older version of this guide used php -i, which reads the CLI php.ini, so on our test system it reported upload_max_filesize => 2M and memory_limit => -1 no matter what the FPM file said:
php-fpm8.4 -i | grep -E "^(memory_limit|upload_max_filesize|post_max_size|opcache.enable) "
The FPM side shows the new values:
memory_limit => 256M => 256M
post_max_size => 64M => 64M
upload_max_filesize => 64M => 64M
opcache.enable => On => On
That reads the file on disk. The running FPM workers only pick it up after the restart in Step 4.
Step 4: Configure PHP-FPM Pool
PHP-FPM manages a pool of worker processes that handle incoming PHP requests. The default pool configuration lives in the www.conf file.
sudo vim /etc/php/8.4/fpm/pool.d/www.conf
Key settings to adjust:
; Process manager type - 'dynamic' is the best choice for most servers
pm = dynamic
; Maximum number of child processes - set based on available RAM
; Formula: (Total RAM - RAM for OS and other services) / Average PHP process size
; For a server with 4GB RAM, 50 is a reasonable starting point
pm.max_children = 50
; Number of child processes created on startup
pm.start_servers = 5
; Minimum number of idle processes
pm.min_spare_servers = 5
; Maximum number of idle processes
pm.max_spare_servers = 35
; Number of requests each child process should execute before respawning
; Helps prevent memory leaks from accumulating
pm.max_requests = 500
If your server has limited RAM (1-2 GB), use more conservative values:
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
Test the pool and php.ini syntax before touching the running service, so a typo shows up here instead of as a failed restart:
sudo php-fpm8.4 -t
It ends with configuration file /etc/php/8.4/fpm/php-fpm.conf test is successful.
Enable and Restart PHP-FPM
The package already started PHP-FPM during the install, so start would do nothing and your php.ini and pool changes would stay unloaded. Enable it at boot and restart it:
sudo systemctl enable php8.4-fpm
sudo systemctl restart php8.4-fpm
Verify that PHP-FPM is running:
sudo systemctl status php8.4-fpm
Look for enabled on the Loaded line, active (running) with a fresh start time, and the “Ready to handle connections” status. This is the top of the output on Debian 13:
β php8.4-fpm.service - The PHP 8.4 FastCGI Process Manager
Loaded: loaded (/usr/lib/systemd/system/php8.4-fpm.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-10-22 20:40:25 UTC; 433ms ago
Invocation: a0ed1965d6734d11b772cb862ded1f9e
Docs: man:php-fpm8.4(8)
Process: 12057 ExecStartPost=/usr/lib/php/php-fpm-socket-helper install /run/php/php-fpm.sock /etc/php/8.4/fpm/pool.d/www.conf 84 (code=exited, status=0/SUCCESS)
Main PID: 12051 (php-fpm8.4)
Status: "Ready to handle connections"
Also confirm the FPM socket exists:
ls -la /run/php/php8.4-fpm.sock
The socket belongs to www-data, the user both Nginx and Apache run as on Debian:
srw-rw---- 1 www-data www-data 0 Oct 22 20:40 /run/php/php8.4-fpm.sock
If the socket is missing, PHP-FPM did not start; the Troubleshooting section shows where to look.
Step 5: Integrate PHP 8.4 with Nginx
Pick one web server, Nginx here or Apache in Step 6. For server blocks, TLS and tuning beyond PHP, see the Nginx setup guide for Debian. Both packages start their service on port 80 as soon as they install, so the second one fails to start while the first holds the port. On our Debian 12 system with Apache already running, the Nginx install finished but the service came up failed with nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use). If you are moving from Apache to Nginx, run sudo systemctl disable --now apache2 first.
Install Nginx (skip if already installed):
sudo apt install -y nginx
Create a virtual host configuration:
sudo vim /etc/nginx/sites-available/example.conf
Add the following configuration (replace example.com with your domain):
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
Enable the site and test the Nginx configuration:
sudo ln -s /etc/nginx/sites-available/example.conf /etc/nginx/sites-enabled/
sudo nginx -t
The test should report both lines as ok:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
If the test passes, reload Nginx:
sudo systemctl reload nginx
Nginx is now handing .php requests for example.com to the FPM socket. Step 7 proves it with a test page.
Step 6: Integrate PHP 8.4 with Apache
If you’re running Apache instead, the same port rule applies in reverse. With Nginx already on port 80, our Debian 13 system installed apache2 but the service failed with (98)Address already in use: AH00072: make_sock: could not bind to address 0.0.0.0:80. Run sudo systemctl disable --now nginx first if you are switching. Apache’s own proxy_fcgi module talks to PHP-FPM, so the package alone is enough:
sudo apt install -y apache2
Enable the required Apache modules and the PHP-FPM handler config that php8.4-fpm ships:
sudo a2enmod proxy_fcgi setenvif rewrite
sudo a2enconf php8.4-fpm
A server that already runs mod_php (libapache2-mod-php8.4, the module a bare apt install php8.4 selects) needs one more change, because mod_php forces the prefork MPM. Switch in this order, FPM side first and mod_php off last, so a failure halfway never leaves Apache serving your PHP source as plain text:
sudo apt install -y php8.4-fpm && sudo a2enmod proxy_fcgi setenvif && sudo a2enconf php8.4-fpm && sudo a2dismod php8.4 mpm_prefork && sudo a2enmod mpm_event && sudo systemctl restart apache2
On our Debian 12 test system a2query -M then answered event, and a PHP page reported the fpm-fcgi SAPI. A fresh install from this guide never loads mod_php, so it skips that line.
Create the document root first, so the config test below doesn’t warn AH00112: Warning: DocumentRoot [/var/www/example.com] does not exist:
sudo mkdir -p /var/www/example.com
Create a virtual host configuration:
sudo vim /etc/apache2/sites-available/example.conf
Add the following (replace example.com with your domain):
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com
<Directory /var/www/example.com>
AllowOverride All
Require all granted
</Directory>
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.4-fpm.sock|fcgi://localhost"
</FilesMatch>
ErrorLog ${APACHE_LOG_DIR}/example_error.log
CustomLog ${APACHE_LOG_DIR}/example_access.log combined
</VirtualHost>
Enable the site and test Apache’s configuration:
sudo a2ensite example.conf
sudo apachectl configtest
If you see Syntax OK, restart Apache. A reload is not enough here: a2enmod asks for a restart to load proxy_fcgi, and on a server where Apache failed to start because of the port clash, systemctl reload apache2 only answers apache2.service is not active, cannot reload.
sudo systemctl restart apache2
Check that Apache stayed up with systemctl is-active apache2, which should print active.
Step 7: Test with phpinfo()
Create a test file to confirm everything is working together:
sudo mkdir -p /var/www/example.com
echo '<?php phpinfo(); ?>' | sudo tee /var/www/example.com/info.php
Set the correct ownership:
sudo chown -R www-data:www-data /var/www/example.com
Request the page by the site’s name, not the server’s IP address. Both web servers keep their default site, which answers requests for a bare IP, so http://10.0.1.50/info.php lands there instead; Nginx returned 404 in our test even though PHP worked, which is easy to mistake for a broken PHP setup. With DNS in place, open http://example.com/info.php in a browser. Before DNS exists, ask from the server itself and send the host name along:
curl -s -H "Host: example.com" http://127.0.0.1/info.php | grep -oE 'PHP Version [0-9.]+|FPM/FastCGI' | sort -u
Through Nginx and through Apache alike, on both releases, the page reported the patched version and the FPM handler:
FPM/FastCGI
PHP Version 8.4.27
Important: Remove the info.php file after testing. It exposes detailed server information that you don’t want publicly accessible:
sudo rm /var/www/example.com/info.php
For a quick version check later, such as after the next PHP security update, a page that only prints PHP_VERSION and PHP_SAPI gives away far less than phpinfo.
Step 8: Manage Multiple PHP Versions with update-alternatives
If you have applications that still require an older PHP version, you can install multiple versions side by side and switch between them using update-alternatives. Debian 13’s archive has only PHP 8.4, so a second version needs the Sury repository from Step 1 on both releases. Without it, Debian 13 answers Error: Unable to locate package php8.3-fpm.
For example, to install PHP 8.3 alongside 8.4 (Sury’s 8.3.36 build carries the PHP2Shell fix too):
sudo apt install -y php8.3 php8.3-cli php8.3-fpm
The packages register each php binary with update-alternatives themselves, at priority 84 and 83, so there is nothing to add by hand. The older version of this guide ran update-alternatives --install here, which only made update-alternatives warn that the link group was broken and reinstall it. In auto mode the highest priority wins, so the CLI stays on 8.4 until you change it. List what is registered:
update-alternatives --list php
Both binaries are there:
/usr/bin/php8.3
/usr/bin/php8.4
Switch between versions interactively:
sudo update-alternatives --config php
This shows a menu where you select which PHP version to use as the default CLI version. Choose the version you want and press Enter.
Or set a specific version directly:
sudo update-alternatives --set php /usr/bin/php8.4
Verify the active version:
php -v
For web server integration, you control which PHP version handles requests by pointing your Nginx or Apache configuration to the correct FPM socket, for example /run/php/php8.4-fpm.sock or /run/php/php8.3-fpm.sock. This means different virtual hosts can use different PHP versions. Each version has its own changelog and its own FPM unit, so run the PHP2Shell check and the restart for every version you keep, for example php8.3-common against 8.3.36 and sudo systemctl restart php8.3-fpm. If one of your applications is pinned to PHP 8.2, the PHP 8.2 guide for Debian covers that branch.
Troubleshooting
PHP-FPM Fails to Start
Check the logs for specific errors:
sudo journalctl -xeu php8.4-fpm
Common causes include syntax errors in pool configuration or port/socket conflicts with another PHP-FPM version. Make sure you don’t have two FPM pools trying to listen on the same socket.
502 Bad Gateway in Nginx
This usually means Nginx can’t reach the PHP-FPM socket. Verify the socket exists and has the right permissions:
ls -la /run/php/php8.4-fpm.sock
sudo systemctl status php8.4-fpm
Also check that the fastcgi_pass path in your Nginx config matches the actual socket path.
Extensions Not Loading
If php -m doesn’t show an extension you installed, check if the extension’s .ini file exists:
ls /etc/php/8.4/mods-available/
Enable it manually if needed, using the file name without .ini (redis here), then restart FPM so the web side loads it too:
sudo phpenmod -v 8.4 redis
sudo systemctl restart php8.4-fpm
Run php -m again afterwards and the module should be in the list. The CLI and FPM read separate conf.d directories under /etc/php/8.4/, but phpenmod without -s links the module into every SAPI, so the one command fixes the web side too once FPM restarts.
Sury Repository GPG Key Errors
If apt update reports a signature error for packages.sury.org, reinstall the current keyring package. Servers set up with the older apt.gpg method from earlier versions of this guide should also move to this keyring and point signed-by= at it, as in Step 1:
curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
sudo dpkg -i /tmp/debsuryorg-archive-keyring.deb
sudo apt update
The update should fetch the Sury release file again without the signature error. If it still fails, open /etc/apt/sources.list.d/ and check which keyring each Sury line names in signed-by=, because a leftover line pointing at the old sury-php.gpg keeps failing no matter which keyring you install. Delete that file and keep the single php.list from Step 1.
File Upload Size Errors
If uploads fail for large files, make sure you’ve updated both upload_max_filesize and post_max_size in php.ini, and post_max_size is equal to or larger than upload_max_filesize. If using Nginx, also check the client_max_body_size directive:
# In your Nginx server block
client_max_body_size 64M;
After making any changes, restart PHP-FPM and your web server:
sudo systemctl restart php8.4-fpm
sudo systemctl restart nginx # or apache2
Keep the Nginx limit and the two PHP limits at the same value, otherwise the smallest of the three decides what a user can upload.
OPcache Not Working
Check OPcache on the FPM side. The CLI is the wrong place to look, because Debian ships opcache.enable_cli => Off, so opcache_get_status() from php -r returns false even when the web side caches fine:
php-fpm8.4 -i | grep -E "^opcache.enable "
It should print opcache.enable => On => On. If it shows Off, set opcache.enable = 1 in the FPM php.ini, confirm php8.4-opcache is installed, and restart php8.4-fpm. The phpinfo page from Step 7 shows the same setting as the running workers see it.
Conclusion
You now have PHP 8.4 installed and configured on Debian 13 or Debian 12. The setup covers PHP-FPM with tuned pool settings, OPcache for performance, and integration with either Nginx or Apache. You also know how to manage multiple PHP versions if your projects need them.
For production servers, I’d recommend monitoring your PHP-FPM process usage over time and adjusting pm.max_children based on actual memory consumption. You can check the status page by enabling pm.status_path in your pool configuration.
this repository seems to be offline. apt update return this:
W: Failed to fetch https://packages.sury.org/php/dists/jessie/main/binary-amd64/Packages HttpError404