Deployment guide

PHP2Shell and Docker: rebuild your PHP and WordPress images

Deploy on a VPS Cloud →

Tutorial

PHP2Shell and Docker: rebuild your PHP and WordPress images

Security & Monitoring12 min read12 steps

You updated WordPress last week, restarted the container, and you assume you are covered against PHP2Shell. That is not certain: in a container, PHP lives in the image, not in the application, and a restart reuses the image already on disk. The PHP fixes 8.5.12, 8.4.27, 8.3.36 and 8.2.35 were released on October 21, 2026. This guide shows how to prove, container by container, which PHP really answers, then how to pull or rebuild the image that is not fixed, for WordPress, Nextcloud, Laravel Octane and a custom image.

Contents· Why updating the application does not change PHP1/14
  1. 01Why updating the application does not change PHP
  2. 02Six objects not to confuse
  3. 03Compare the registry, the local image and the container
  4. 04Read the version the web really serves
  5. 05Prove the served version, service by service
  6. 06Floating tags and pinned tags: what moves by itself
  7. 07Which notation moves, which stays frozen
  8. 08Custom image: the sequence and what each option changes
  9. 09Rebuild and recreate a custom image
  10. 10Official WordPress and Nextcloud: PHP comes from the tag
  11. 11Laravel Octane and FrankenPHP: the engine is in the image
  12. 12Infrastructure traps that replay the old image
  13. 13Troubleshooting: what you see, what it means
  14. 14The official documentation

Why updating the application does not change PHP

A WordPress update, a composer update or a new Nextcloud release replaces application files. The PHP interpreter is a program installed in the Docker image, just like the Debian or Alpine system around it. As long as the image does not change, PHP does not change, however many times the application is updated.

The second trap is the restart. docker restart restarts the existing container, so the same image. docker compose up -d does not look for a newer version when an image with that name already exists on the server: it reuses the local copy. Only a pull or a build --pull, followed by recreating the container, changes the interpreter.

The two flaws and the apt or dnf commands are covered in our PHP update guide. Here the subject is the Docker image and the proof that it is the right one.

Six objects not to confuse

  • The registry image — what Docker Hub or your private registry serves today under a tag such as php:8.3-fpm. A tag is mobile: it points to different content after every rebuild.
  • The local image — the copy on your server, visible with docker image ls. It only moves when you pull or build an image.
  • The running container — it keeps the ID of the image it was created from, even if a newer image now carries the same name.
  • The process that answers — PHP-FPM, Apache with mod_php or FrankenPHP. It is the exposed one, and php -v does not query it.
  • The other services — queue worker, scheduler, maintenance cli container: each has its own image, hence its own PHP version.
  • The build cache — a cached FROM layer can replay the old base without any error being shown.

Compare the registry, the local image and the container

  1. List local images and their date

    docker image ls php (or wordpress, nextcloud). The CREATED column shows when the image was built. A php:8.3-fpm image created before October 21, 2026 does not contain the fix.

  2. Read each container's image

    docker ps shows in the IMAGE column the tag requested at creation. To get the real ID of the image in use, query the container: docker inspect --format '{{.Image}}' <container>.

  3. Compare with the image on disk

    docker image inspect --format '{{.Id}}' php:8.3-fpm returns the ID of the image that carries that name today. If it differs from the container's, the container runs on an image older than the one on disk: it was never recreated after the last pull.

  4. Compare with the registry

    docker pull php:8.3-fpm ends with Status: Downloaded newer image for … when a newer image arrived, or Status: Image is up to date for … otherwise. In the second case, either your copy is already the latest or the tag has not been republished yet: check it against the tag date on Docker Hub.

Read the version the web really serves

php -v queries the command-line program, not the process that answers requests. The PHP_SAPI constant tells which one is speaking: cli on the command line, fpm-fcgi for PHP-FPM, apache2handler for Apache with mod_php. In a single image the two versions almost always match. The gap appears in three common Docker situations: a docker compose run starts a fresh container, so from the latest local image, while the web service still runs the old one; a maintenance cli container is built on a different tag than the web service; a reverse proxy forwards to a different container than the one you inspect.

The most reliable proof remains the HTTP request: it follows a visitor's path and reads PHP_VERSION in the process that answers. Check the FPM binary as a complement with docker exec <container> php-fpm -v.

Prove the served version, service by service

  1. Drop a test file in the served folder

    Example with the wordpress service of the official image, whose web folder is /var/www/html (adapt the service and path to your stack): docker compose exec wordpress sh -c 'echo "<?php echo PHP_VERSION, \" \", PHP_SAPI;" > /var/www/html/v-check.php'

  2. Query it over HTTP, then delete it right away

    curl -s https://your-domain.com/v-check.php then docker compose exec wordpress rm /var/www/html/v-check.php. The answer gives the version followed by the SAPI. Compare it with the fixed versions: 8.5.12, 8.4.27, 8.3.36 or 8.2.35 depending on the branch. A lower number for your branch means the fix is not there.

  3. Check the container's FPM binary

    docker exec <container> php-fpm -v prints the version and SAPI of that specific container's FPM binary. With an Apache image, the HTTP request from the previous step is enough: mod_php is loaded by Apache.

  4. Do not trust docker compose run

    docker compose run creates a fresh container from the local image. It can report the fixed version while the site's container has not moved. Always check the container that receives traffic.

Floating tags and pinned tags: what moves by itself

A branch tag such as php:8.3-fpm advances at the next pull: it then points to the fixed version. An exact tag such as php:8.3.36-fpm stays on its version, and a digest reference (@sha256:…) never moves. The docker image pull documentation is explicit: with a digest, Docker fetches no rebuild, security fixes included, until you change the digest yourself.

The fixed tags of the official php image reached Docker Hub during the day and evening of October 21, 2026 (UTC), for the FPM variants of branches 8.5, 8.4, 8.3 and 8.2. The official WordPress images php8.3-fpm, php8.4-fpm and php8.3-apache were republished on October 22, 2026 in the early morning (UTC), and Nextcloud's the same day. An image pulled before those moments is therefore the old one, even with an identical name. Check these dates on each tag's page. Branch 8.2 gets its fix but reaches end of life on December 31, 2026: plan its replacement.

Which notation moves, which stays frozen

Scroll the table

In your compose file or DockerfileMoves at the next pull?To get the fixed PHP
`php:8.3-fpm`Yes, the tag is republished`pull` or `build --pull`, then recreate the container
`php:8.3.36-fpm`No, exact versionAlready fixed; change it by hand at the next fix
`php:8.3.<n>-fpm` with n below 36No, exact versionMove to a fixed tag, rebuild, recreate
`wordpress:php8.3-fpm`Yes, the maintainers republish it`pull`, then recreate the service
`wordpress:<x.y>-php8.3-fpm`Yes for PHP: the maintainers republish it`pull`, then recreate the service
`php:8.3-fpm@sha256:…`NeverReplace the digest with that of a fixed image
`FROM php:8.3-fpm` in your DockerfileOnly if you build with `--pull``build --pull`, then recreate the container
`image: myregistry/app:latest` (your image)No, it is your last buildRebuild, republish, then pull and recreate

Custom image: the sequence and what each option changes

For an image built in-house from FROM php:8.3-fpm, the sequence takes three commands: docker compose build --pull --no-cache, then docker compose up -d --force-recreate, then a check of the served version. Each answers a specific trap.

--pull is the option that changes the PHP version: it tells Docker to look for a newer version of the base image instead of reusing the server's. Without it, the FROM is resolved with the local copy, and the build succeeds with the old interpreter, without a single message.

--no-cache replays every layer, including apt-get and composer install, which also renews system libraries and dependencies. It is not needed to change PHP, because when the base image changes the following layers are rebuilt on their own. It is useful when a cached layer freezes a package you want renewed.

--force-recreate recreates containers even if Compose considers their configuration and image unchanged. That removes the doubt about what is running.

Rebuilding your images at each fix is a schedule to keep, not a one-off action. On a VPS with root access, that schedule is yours, and our guide to installing Docker on a VPS lays the groundwork.

Rebuild and recreate a custom image

  1. Check that the base tag is fixed

    Open your base tag's page on Docker Hub and check that it was published from October 21, 2026. If your FROM targets an older exact version, change it first.

  2. Rebuild while pulling the base

    docker compose build --pull --no-cache. Note the version before the operation with the test file, to have a before and an after.

  3. Recreate the containers

    docker compose up -d --force-recreate. A single command can also chain the three steps: docker compose up -d --build --pull always. Recreate every service that uses the image: web, queue worker, scheduler.

  4. Prove the result

    Run the test file again, then compare the ID given by docker inspect --format '{{.Image}}' <container> with that of docker image ls. The old and new images can carry the same name: only the ID and the HTTP answer are proof.

Official WordPress and Nextcloud: PHP comes from the tag

A WordPress tag combines a WordPress version, a PHP version and a variant, for example php8.3-apache or php8.3-fpm. The PHP branch is chosen by the tag name. Changing the WordPress version, or letting automatic updates act in the data volume, does not touch that branch. To get the fixed PHP, re-pull the tag with docker compose pull, then recreate the service. The Apache variant is the default one, and it loads PHP as mod_php: the PHP project considers it the most exposed, with an exploit demonstrated before any application code runs. Treat it as a priority.

Nextcloud follows the same principle, in two variants: apache ships a full Apache, fpm needs a web server in front of it. The image documentation describes the update as a replacement: pull the image, throw away the old container, start the new one. Nextcloud's code and your data live in the /var/www/html volume, which recreating the container does not erase. See also our WordPress on VPS guide and the Nextcloud one.

Laravel Octane and FrankenPHP: the engine is in the image

With Octane and FrankenPHP there is no PHP-FPM: the application server embeds the interpreter. The official FrankenPHP images are built on the official PHP images, and their tag combines the FrankenPHP version, the PHP version and the system, in the form dunglas/frankenphp:<version>-php8.4-trixie. The project rebuilds them at each release and, every day at 4 am UTC, if new official PHP images exist. Your FROM dunglas/frankenphp only gets the fix if you rebuild your image with --pull.

The trap specific to Octane is the worker. Octane boots the application once and keeps it in memory: php artisan octane:reload gracefully restarts the workers to load the deployed code. It is the right move after a code deployment, but it does not replace the interpreter, which remains that of the running image. The same goes for a kill -USR2 or a PHP-FPM reload run in the old container: they restart the same files, those of the old image. To change PHP, recreate the container, including those of queue workers and the scheduler. See our Laravel Octane on VPS guide.

Infrastructure traps that replay the old image

  • CI build cache — a restored layer cache can replay the FROM layer. Add --pull to the CI build command, or invalidate the cache for that step.
  • Private registry or mirror — if your servers pull from an internal registry that copies images by hand or on a schedule, check that it copied the tag. Otherwise pull answers Image is up to date while the maintainers have republished.
  • Portainer stack or orchestration tool — updating a stack does not always re-pull the image. Tick the re-pull option before redeploying if your tool offers one.
  • pull_policy: missing — it is Compose's default without a build section: it only pulls if the image is absent from the server. Only the latest tag is pulled in every case. Use always, or daily and weekly to check the registry at a fixed interval.
  • A forgotten service — web, worker, scheduler and cli container built from separate Dockerfiles each have their own base: fix them all.
  • An orphan container — a forgotten docker run or another Compose project may still answer on a port. List everything running with docker ps.

Troubleshooting: what you see, what it means

Scroll the table

What you noticeProbable causeRemedy
`pull` prints `Status: Image is up to date` but the version stays oldThe tag has not been republished yet, or you pull an exact version or a digestCompare with the tag date on Docker Hub, then move to a fixed tag
`up -d` changes nothing after a pullThe existing container was not recreated`docker compose up -d --force-recreate`, then compare image IDs
The build finishes in a few seconds, with the base already cached`--pull` is missing: the server's base is reused`docker compose build --pull`, with `--no-cache` if system packages must also be renewed
`docker compose run` reports the fixed version, the site answers the old one`run` creates a fresh container, the web service was not recreatedCheck the container that receives traffic over HTTP, then recreate it
The version is right but a worker behaves as beforeA queue or Octane worker still runs in an old containerRecreate every container that uses the image, not only the web service

The official documentation

The sources: PHP security and release announcements, docker compose up, docker compose build, the pull_policy attribute, docker image pull, the php image, the wordpress image, the Nextcloud image, FrankenPHP with Docker and Laravel Octane.

A VPS where you own your images and their schedule

On a ServOrbit.com VPS, you get root access and a dedicated IPv4: your Docker images, their tags and the timing of each rebuild are yours. A VPS administration option also exists; its terms are described on the offer page.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab