Chat on WhatsApp
Upgrades & Patches 10 min read

Patching PHP in Docker: How to Update PHP in a Docker Container After php2shell

Running Magento, WordPress or any PHP app in Docker? The php2shell security release will not reach your containers through the host. Here is how I update PHP in a Docker container: pull or rebuild, recreate, verify php -v, and close dev ports on the LAN.

Patching PHP in Docker: How to Update PHP in a Docker Container After php2shell

To update PHP in a Docker container you have to rebuild or re-pull the image and recreate the container. Updating packages on the host does not touch it. I am writing this on 21 October 2026, the day of the critical PHP security release for php2shell (CVE-2026-109618 and CVE-2026-109619), because I keep seeing people run apt upgrade on a Docker host and assume they are covered.

They are not. My own store, kishansavaliya.com, runs PHP 8.4.21 inside a custom image built FROM php:${PHP_VERSION}-fpm with PHP_VERSION=8.4. No host package update has ever changed that number. Here is the exact routine I use to patch PHP in Docker, on the server and on my Mac.

15PHP containers on my Mac, all behind on patches
13compose projects they came from
4dev containers reachable from the office Wi-Fi
3PHP containers on my live VPS

What happened on 20 and 21 October 2026?

On 20 October the PHP team posted an advance notice on php.net: a security release scheduled for Wednesday 21 October 2026 at 14:00 UTC, fixing two vulnerabilities, both rated CVSS v4.0 9.2. The fixes shipped on 21 October as PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Huntress wrote up the background: details leaked on Hacker News before coordinated disclosure, the bugs are being called php2shell, and watchTowr confirmed they are real. As of Huntress's post there was no public proof of concept, but I would not plan around that staying true.

The fixed versions are 8.5.12, 8.4.27, 8.3.36 and 8.2.35, and my php2shell write-up explains the bugs. Compare those numbers with what your containers report. If you also want to know which PHP versions you even have, my sister post on finding every PHP version on your server covers the inventory side.

Why doesn't apt upgrade on the host patch PHP in Docker?

Because the container never uses the host's PHP. The official php image compiles PHP from source and installs it inside the image. Docker Hub's page for the image even says it blocks Debian's own PHP packages to avoid two conflicting installs. So the host could have no PHP at all, which is exactly my Mac: there is no host PHP, and php in my shell is a function that runs PHP inside the project's container.

On my VPS the picture is the same. Besides the Magento image, there is a WordPress image pinned to a php8.3 tag at 8.3.31 and a php:8.3-apache image at 8.3.31. Three containers, three separate PHP builds, and none of them cares what apt does on Ubuntu.

Tags move, your local copy does not

A tag like php:8.4-fpm or wordpress:6.9.4-php8.3-apache is a pointer. When the maintainers rebuild the image for a new PHP patch release, the tag on Docker Hub points at the new build. Your server still has the image it pulled weeks ago, under the same tag name. Nothing changes until you pull again.

This is what catches people. The Dockerfile says 8.4, the compose file says 8.4, everything looks current. Then you run php -v inside the container and get a version from two releases ago. That is what I found on my Mac today: 15 local PHP containers across 13 compose projects, ranging from PHP 8.2.30 to 8.4.22, and every single one was behind even the previous security releases from 24 September 2026.

Has the official image been rebuilt yet?

Check this before you rebuild, or you will rebuild onto the same old PHP and feel safe for no reason. The official images are maintained in the docker-library/php GitHub repo, but the source of truth for what Docker Hub builds is the library/php file in docker-library/official-images. A change merged in the php repo does not appear on Docker Hub until that file is updated and the build runs. The official-images README promises version bumps and security fixes "in a timely manner" and says security pull requests are expedited, but it gives no fixed timeframe.

So there is a gap between php.net publishing and the tag moving. Two ways to check:

  • Look at the tag's last pushed time on the Docker Hub tags page for php.
  • Pull the tag and ask it directly:
docker pull php:8.4-fpm
docker run --rm php:8.4-fpm php -r 'echo PHP_VERSION, PHP_EOL;'

If that still prints the old version, wait and check again. Rebuilding early just wastes a build.

How do I update PHP in a Docker container with docker compose build --pull?

For a service that has a build: section and a Dockerfile starting FROM php:..., this is the whole fix:

docker compose build --pull php
docker compose up -d php

The Docker docs describe --pull as "Always attempt to pull a newer version of the image". Without it, Docker happily reuses the base image it already has. A plain docker restart or docker compose restart does nothing here: it restarts the same container from the same old image. Custom images built FROM php need a rebuild, not a restart. up -d then sees the new image and recreates the container.

For a service that only has image: (no build), such as a stock WordPress container, pull instead:

docker compose pull wordpress
docker compose up -d wordpress

If you pin a full tag like wordpress:6.9.4-php8.3-apache, that tag moves when the image is rebuilt for a new PHP patch, the same as the short tags. Pull it and check the version anyway. Do not assume.

When do you need --no-cache?

If the base image digest changes, Docker invalidates every layer after FROM on its own. You do not need --no-cache for the normal case.

You do need it when PHP does not come from the FROM line. For example, an image built on debian or ubuntu that installs PHP with an apt-get install in a RUN step, or one that downloads a PHP tarball by URL. Those layers are cached, the base did not change, so the old PHP stays. The docs describe --no-cache simply as "Do not use cache when building the image":

docker compose build --pull --no-cache php
docker compose up -d php

It is slower, especially with PECL extensions that compile. Fine. This is a security release.

Check the real version inside the container

Never trust the tag name, the Dockerfile, or a dashboard. Ask PHP:

docker exec <container> php -v
docker exec <container> php -r 'echo PHP_VERSION, PHP_EOL;'

You want 8.5.12, 8.4.27, 8.3.36 or 8.2.35 for your branch. If you run PHP-FPM, also remember the CLI and FPM binaries come from the same build in the official image, so the CLI check is a fair proxy. If you built PHP some other way, check php-fpm -v too.

Tags or digests?

A tag is a moving name. A digest (sha256:...) is a fixed image. Pinning a digest gives you repeatable builds, and it also means you will never get a security fix until someone changes the digest. My opinion: for a small team, a minor tag like php:8.4-fpm plus a rebuild habit is better than a digest nobody updates. If you do pin digests, let a bot such as Dependabot or Renovate open the pull request when the tag moves.

# moves with every rebuild of 8.4
FROM php:8.4-fpm

# frozen until you change it
FROM php:8.4-fpm@sha256:<digest>

To see what digest you actually have locally, run docker images --digests php.

Make it one command next time

The real lesson from today is that PHP patching inside containers should not depend on someone remembering. I keep a small script in each project and the deploy pipeline calls it:

#!/usr/bin/env bash
set -euo pipefail
docker compose build --pull php
docker compose up -d php
docker compose exec -T php php -r 'echo PHP_VERSION, PHP_EOL;'

Run it on a schedule in CI as well, and not just when the code changes. A Dockerfile that has not changed in months still needs fresh PHP every time upstream ships a security release. If your pipeline only builds when the code changes, a quiet project will sit on old PHP forever.

Don't forget local dev machines

People skip laptops because "it's only local". But a dev container with ports published on 0.0.0.0 is reachable by anyone on the same network. When I checked, four of my local dev containers were reachable from the office Wi-Fi. Find them with:

docker ps | grep 0.0.0.0

Fix it by binding to loopback in the compose file:

services:
  web:
    ports:
      - "127.0.0.1:8080:80"

For a compose file tracked in a client repo, I did not want to change their file. Docker Compose loads docker-compose.override.yml automatically, and the !override tag replaces the ports list instead of merging it. Without !override, Compose keeps both entries and the 0.0.0.0 one stays open:

services:
  web:
    ports: !override
      - "127.0.0.1:8080:80"

Then keep the override out of git without touching their .gitignore:

echo "docker-compose.override.yml" >> .git/info/exclude

After that, the same build --pull and up -d routine on every local project.

Magento after-steps

For a Magento store, recreating the PHP container does a few things for you and leaves a few for you to check:

  • OPcache resets. A recreated container starts fresh PHP-FPM processes with an empty OPcache, so there is no stale bytecode to worry about.
  • Check docroot permissions. Once, after recreating the PHP container on my live server, a permission change on the docroot made nginx return 404 for every URL. The fix was chmod o+rx on the docroot. Check this after any recreate.
  • Run a smoke test. Homepage, a category, a product, cart, the admin login page, and bin/magento cron:run inside the container. A quick curl -sI on each URL tells you most of what you need.
stat -c %a /path/to/docroot
curl -sI https://example.com/ | head -1

And patching is not the same as being clean. If your store was exposed before you patched, read how to check a Magento server for PHP RCE compromise and look for PHP workers spawning shells. If you are rethinking your PHP runtime anyway, my FrankenPHP worker mode guide covers a different container setup where the same rebuild rule applies.

Want a second pair of eyes on your containers? I can check every PHP container on your server, rebuild the images and smoke test the store, at $25/hr.

Hire me

FAQ

Does apt upgrade on the host update PHP inside Docker?

No. The official php image builds its own PHP inside the image. Host packages are never used by the container. You have to pull or rebuild the image and recreate the container.

Is docker restart enough after a PHP security release?

No. Restart reuses the same image. Run docker compose build --pull for built services or docker compose pull for image-only ones, then docker compose up -d to recreate the container.

How do I check which PHP version my container runs?

Run docker exec followed by the container name and php -v, or php -r 'echo PHP_VERSION;'. You want 8.5.12, 8.4.27, 8.3.36 or 8.2.35.

Why does my rebuilt image still show the old PHP version?

Either the official tag has not been rebuilt yet, or PHP comes from a cached RUN layer instead of the FROM image. Check the tag's last pushed time on Docker Hub, and use --no-cache if PHP is installed in your own layer.

Does a pinned tag like wordpress:6.9.4-php8.3-apache get PHP fixes?

The tag moves when the maintainers rebuild it for new PHP releases, but your local copy only changes after you pull. Pull it and verify the version inside the container.

Should I pin images by digest?

Only if something updates the digest for you, such as Dependabot or Renovate. A digest nobody touches means you never receive security fixes.

Do local dev containers need patching too?

Yes, especially if ports are published on 0.0.0.0, because anyone on the same network can reach them. Bind dev ports to 127.0.0.1 and rebuild them like production.