CentOS 7 reached end of life in June 2024, but some existing servers still need to run it. In this article, I describe how I built PHP 8.2.35 for CentOS 7.9 x86_64, compiled several PECL extensions, and prepared the files for deployment to an Apache server.
The idea is to build PHP in a Docker container based on CentOS 7-compatible tooling, then transfer the resulting files to the target server. This avoids having to install all the build dependencies directly on the production machine.
Why I Needed PHP 8.2.35
On October 21, 2026, the PHP project released PHP 8.2.35 to address two critical remote code execution vulnerabilities: CVE-2026-109618 and CVE-2026-109619. Both affected PHP 8.2 releases earlier than 8.2.35 and could allow an unauthenticated attacker to execute code on a vulnerable server.
The vulnerabilities involved memory handling inside PHP itself. One concerned memory corruption during out-of-memory handling; the other involved stale internal state referencing memory that had already been freed. The PHP security team identified Apache with mod_php as a particularly high-risk configuration because a malicious request could trigger an exploit before application code ran.
Sources: PHP 8.2.35 release announcement and PHP security advisory.
My server was running an older PHP 8.2 release with Apache’s mod_php. Since the standard PHP packages available on the legacy CentOS 7 system did not provide the required patched version, I decided to build PHP 8.2.35 from source in Docker and deploy it separately under /opt/php-8.2.35.
This approach allowed me to keep the existing application stack while installing a PHP version containing the security fixes.
CentOS 7 itself is no longer supported, so this should be considered a compatibility measure for a legacy system, not a long-term replacement for upgrading the operating system.
1. Prepare the Docker build environment
I used the manylinux2014_x86_64 image as the starting point and configured CentOS 7.9 archive repositories inside it. The complete Dockerfile is available in this GitHub Gist.
On Windows, create a working directory and save the Dockerfile there:
mkdir C:\php-build
cd C:\php-build
Build the image for the x86_64 architecture:
docker build --platform linux/amd64 -t php82-centos7-builder .
Start a container:
docker run -it --name php82-builder php82-centos7-builder
The Dockerfile configures the CentOS 7.9 and EPEL 7 archive repositories, installs the development dependencies, and makes GCC from Developer Toolset 10 available. It also builds re2c 1.0.3 from source.
Inside the container, the build environment can be checked with:
cat /etc/centos-release
gcc --version
rpm -q httpd httpd-devel
apxs -q LIBEXECDIR
apxs -q INCLUDEDIR
The Apache development package is important because PHP is built with apxs to produce the Apache module.
2. Download and configure PHP
Download the PHP 8.2.35 source archive from the official PHP downloads.
Extract it and enter the source directory. The commands below assume that the source is in /usr/local/src/php-8.2.35.
cd /usr/local/src
curl -fL --retry 3 \
https://www.php.net/distributions/php-8.2.35.tar.gz \
-o php-8.2.35.tar.gz
tar -xzf php-8.2.35.tar.gz
cd php-8.2.35
The build uses the prefix /opt/php-8.2.35 and enables the extensions needed by the application:
cd /usr/local/src/php-8.2.35
PKG_CONFIG_PATH=/usr/lib64/pkgconfig \
SQLITE_CFLAGS="-I/usr/include" \
SQLITE_LIBS="-L/usr/lib64 -lsqlite3" \
./configure \
--prefix=/opt/php-8.2.35 \
--with-apxs2=/usr/bin/apxs \
--with-config-file-path=/opt/php-8.2.35/etc \
--with-config-file-scan-dir=/opt/php-8.2.35/etc/conf.d \
--enable-bcmath \
--enable-calendar \
--enable-exif \
--enable-ftp \
--enable-mbstring \
--enable-pcntl \
--enable-posix \
--enable-shmop \
--enable-sockets \
--enable-soap \
--enable-sysvmsg \
--enable-sysvsem \
--enable-sysvshm \
--enable-opcache \
--enable-gd \
--with-bz2 \
--with-curl \
--with-gmp \
--with-openssl \
--with-readline \
--with-zlib \
--with-mysqli=mysqlnd \
--with-pdo-mysql=mysqlnd \
--with-pdo-sqlite \
--with-sqlite3 \
--with-jpeg \
--with-freetype \
--with-snmp \
--with-xsl \
--with-gettext \
--with-sodium
On this legacy system, I also applied a compatibility patch to ext/openssl/xp_ssl.c before compiling:
sed -i '/#include "php_openssl.h"/a #if OPENSSL_VERSION_NUMBER < 0x10100000L\n#define ASN1_STRING_get0_data ASN1_STRING_data\n#endif' ext/openssl/xp_ssl.c
Compile and install PHP:
make -j2
make install
I did not enable ZTS. The resulting build is intended for Apache with mpm_prefork and the PHP Apache module (mod_php).
3. Build the PECL extensions
I also needed the ds, mongodb, and xmlrpc extensions. They are built separately using the phpize and php-config tools from the new PHP installation.
DS
cd /usr/local/src
curl -fLO https://pecl.php.net/get/ds-1.8.0.tgz
tar -xzf ds-1.8.0.tgz
cd ds-1.8.0
/opt/php-8.2.35/bin/phpize
./configure --with-php-config=/opt/php-8.2.35/bin/php-config
make -j2
make install
MongoDB
cd /usr/local/src
curl -fL https://pecl.php.net/get/mongodb -o mongodb.tgz
tar -xzf mongodb.tgz
cd mongodb-*
/opt/php-8.2.35/bin/phpize
./configure \
--with-php-config=/opt/php-8.2.35/bin/php-config \
--enable-mongodb
make -j2
make install
XML-RPC
cd /usr/local/src
curl -fL https://pecl.php.net/get/xmlrpc -o xmlrpc.tgz
tar -xzf xmlrpc.tgz
cd /usr/local/src/xmlrpc-*
/opt/php-8.2.35/bin/phpize
./configure --with-php-config=/opt/php-8.2.35/bin/php-config
make -j2
make install
The MongoDB and XML-RPC commands download the current PECL release rather than a pinned version. For a repeatable build, choose and record the exact extension versions you need.
4. Configure PHP extensions
Create the configuration directory, copy the production php.ini template, and enable the extensions:
mkdir -p /opt/php-8.2.35/etc/conf.d
cp /usr/local/src/php-8.2.35/php.ini-production \
/opt/php-8.2.35/etc/php.ini
cat > /opt/php-8.2.35/etc/conf.d/20-extensions.ini <<'EOF'
extension=ds.so
extension=mongodb.so
extension=xmlrpc.so
zend_extension=opcache.so
EOF
Check the configuration and loaded modules:
/opt/php-8.2.35/bin/php --ini
/opt/php-8.2.35/bin/php -v
/opt/php-8.2.35/bin/php -m
The extension configuration directory is defined by --with-config-file-scan-dir. The compiled shared extensions are installed in PHP’s extension directory, which can be checked with:
/opt/php-8.2.35/bin/php-config --extension-dir
5. Create the deployment bundle
Installing PHP in the build container does not automatically create a directory called php-8.2.35-bundle. We need to create the directory and copy the files into it explicitly.
In this container, the relevant paths are:
- PHP installation:
/opt/php-8.2.35 - Apache module:
/usr/lib64/httpd/modules/libphp.so - Apache module directory reported by
apxs:/usr/lib64/httpd/modules
Create the bundle structure:
mkdir -p /tmp/php-8.2.35-bundle/opt
mkdir -p /tmp/php-8.2.35-bundle/usr/lib64/httpd/modules
mkdir -p /tmp/php-8.2.35-bundle/etc/httpd/conf.d
Copy the PHP installation and Apache module:
cp -a /opt/php-8.2.35 \
/tmp/php-8.2.35-bundle/opt/
cp -a /usr/lib64/httpd/modules/libphp.so \
/tmp/php-8.2.35-bundle/usr/lib64/httpd/modules/
Create an Apache configuration file for the PHP handler:
cat > /tmp/php-8.2.35-bundle/etc/httpd/conf.d/php.conf <<'EOF'
<IfModule php_module>
<FilesMatch \.php$>
SetHandler application/x-httpd-php
</FilesMatch>
</IfModule>
EOF
This file configures the handler; it does not load the module by itself. The target server must also have a LoadModule directive pointing to the bundled module. If the server has a separate module configuration such as /etc/httpd/conf.modules.d/20-php.conf, update that configuration as part of deployment. Do not load the old and new PHP modules simultaneously.
The bundle now has this structure:
php-8.2.35-bundle/
├── opt/
│ └── php-8.2.35/
├── usr/
│ └── lib64/
│ └── httpd/
│ └── modules/
│ └── libphp.so
└── etc/
└── httpd/
└── conf.d/
└── php.conf
Create the archive from /tmp, explicitly selecting the bundle directory:
tar -czf /tmp/php-8.2.35-centos7-x86_64.tar.gz \
-C /tmp php-8.2.35-bundle
Verify that the archive exists and contains the expected files:
ls -lh /tmp/php-8.2.35-centos7-x86_64.tar.gz
tar -tzf /tmp/php-8.2.35-centos7-x86_64.tar.gz | head -40
I obtained an archive of approximately 80 MB.
6. Check the build before deployment
Run PHP and inspect the Apache module’s shared-library dependencies:
/opt/php-8.2.35/bin/php -v
/opt/php-8.2.35/bin/php -m
ldd /usr/lib64/httpd/modules/libphp.so | grep 'not found'
If the last command produces no output, ldd did not find missing dependencies in the build container. This does not guarantee that all dependencies are present on the target server, so check them there as well.
7. Copy the archive out of Docker
Open a PowerShell terminal on the Windows host. Replace php82-builder with the actual container name or ID if you used a different one.
docker cp php82-builder:/tmp/php-8.2.35-centos7-x86_64.tar.gz `
"$env:USERPROFILE\Downloads\php-8.2.35-centos7-x86_64.tar.gz"
The archive can now be transferred to the CentOS server using your usual file-transfer method.
8. Deploy on the CentOS server
On the target server, unpack the archive into a temporary directory first:
mkdir -p /tmp/php-deploy
tar -xzf /path/to/php-8.2.35-centos7-x86_64.tar.gz \
-C /tmp/php-deploy
Copy the files to their intended locations:
cp -a /tmp/php-deploy/php-8.2.35-bundle/opt/php-8.2.35 \
/opt/
cp -a /tmp/php-deploy/php-8.2.35-bundle/usr/lib64/httpd/modules/libphp.so \
/usr/lib64/httpd/modules/
cp -a /tmp/php-deploy/php-8.2.35-bundle/etc/httpd/conf.d/php.conf \
/etc/httpd/conf.d/
Make sure Apache loads the new module. For example, /etc/httpd/conf.modules.d/20-php.conf should contain:
LoadModule php_module /usr/lib64/httpd/modules/libphp.so
Keep the existing application-specific Apache settings, such as DirectoryIndex, .phar handling, and session settings, if your configuration uses them. Back up the current Apache configuration before making changes.
Check that Apache uses mpm_prefork and that no other PHP module is loaded:
httpd -M | grep -E 'mpm_|php'
Check PHP’s dynamic-library dependencies on the target server:
ldd /usr/lib64/httpd/modules/libphp.so | grep 'not found'
Resolve any missing dependencies before restarting Apache.
9. SELinux contexts
When PHP is installed under /opt, SELinux may need persistent file-context rules for the PHP installation, configuration files, and Apache module.
On the target server, the rules used for this setup are:
semanage fcontext -a -t usr_t '/opt/php-8\.2\.35(/.*)?'
semanage fcontext -a -t httpd_config_t '/opt/php-8\.2\.35/etc(/.*)?'
semanage fcontext -a -t httpd_modules_t '/usr/lib64/httpd/modules/libphp\.so'
restorecon -Rv /opt/php-8.2.35
restorecon -v /usr/lib64/httpd/modules/libphp.so
If a rule already exists, inspect it before changing it rather than blindly adding a duplicate. Verify the resulting labels with ls -Z and matchpathcon -V.
10. Validate Apache and PHP
Before restarting Apache, test the configuration:
httpd -t
If the configuration is valid, restart Apache and inspect its status:
systemctl restart httpd
systemctl status httpd
Check the PHP CLI:
/opt/php-8.2.35/bin/php -v
/opt/php-8.2.35/bin/php -m
Finally, verify PHP through the web server using a temporary test script. Confirm that the reported version is 8.2.35, the SAPI is apache2handler, and the loaded configuration file is /opt/php-8.2.35/etc/php.ini. Remove the test script after verification.
Keep the previous PHP packages and configuration until the new build has been verified in production. If anything goes wrong, restore the saved Apache configuration and return to the previous module.
Conclusion
Building PHP in a separate CentOS 7-compatible container makes it possible to keep the compiler and development packages off the production server. The key is to package the installed PHP tree, the Apache module, and the handler configuration explicitly—and to verify the dependencies and SELinux labels on the target system.
CentOS 7 is no longer supported, so this approach should be treated as a way to maintain an existing system while planning an upgrade to a supported operating system.