phpqa, a Docker image of PHP quality tools with a deliberate tag policy
Docker image that provides static analysis tools for PHP
At a glance
- What is it?
- The phpqa image packages a catalogue of PHP static analysis tools, with the list of tools and the installer maintained in a separate toolbox repository. What makes it more than a bundle of binaries is its tag policy, which separates tags that are rebuilt daily from tags that follow patch releases from tags that are never touched, plus a build that treats the PHP version and the base distribution as build arguments.
- Who is it for?
- phpqa fits a team that wants one agreed set of PHP quality tools without installing a dozen Composer packages on every machine and without arguing about versions, and the immutable tags make a CI pipeline reproducible. It does not fit a project that needs a tool the image does not carry, or one that wants the tools to match the exact Composer constraints in its own project.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 13 days ago.
- What is it written in?
- Mainly Dockerfile, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The image is the product, the toolbox is the source
phpqa is described as a Docker image providing static analysis tools for PHP, and the first line of substance in the readme is a pointer: the list of available tools and the installer are actually managed in the jakzal/toolbox repository.
That single sentence explains the whole architecture. The image is a build artefact, and the decisions about which tools exist, which versions they are at, and how they are installed live in another repository. The phpqa repository contains a Dockerfile, a Makefile, a README, a docs/ directory and the small amount of glue that turns the toolbox into a container image.
The consequence for a user is a clean separation of concerns. If you want to know what is inside the image and why, the toolbox is the place to look, and it is a project that is designed to be read. If you want to know how the image is built and how it is versioned, this repository is the place to look.
It also means the catalogue moves independently of the image. The readme in this repository is generated, and the Makefile has a target that regenerates it from the toolbox, so the table of tools you read in the readme is not maintained by hand. The readme's own description of the repository, that the list of available tools and the installer are managed in the toolbox, is a warning that the interesting code is not here, and it is the kind of warning that saves an afternoon of digging.
For a team that wants to review the tools it runs in CI, the toolbox repository is the artefact to audit, and this image is a convenient way to run exactly that set.
Three kinds of tag, and what each one promises
The tag policy is the part of the readme that is most useful and most easily missed, because it is stated in three short lines.
Updated daily are the floating tags: latest, debian, alpine, php8.5, php8.5-alpine and the equivalents. These are rebuilt every day, so they pick up new tool releases without any change on your side, and they are what a developer will use locally when they want the current state.
Updated on a patch version change are the minor tags, 1.61, 1.61-php8.5, 1.61-php8.5-alpine and so on. These move when the image's own version number changes its patch component, which means they track the tools that ship as part of a release.
Never updated are the full version tags, 1.61.0, 1.61.0-php8.5, 1.61.0-php8.5-alpine. A tag of that shape is immutable, and it is the one to pin in a pipeline that has to give the same answer next month.
Three tags of the same image, three different promises, and a team that does not know the difference will either update unexpectedly or freeze onto a version with known bugs in the tools. The current version is 1.125.8, and the readme documents the whole matrix for the supported PHP versions, plus a legacy section for the tags of PHP versions that are no longer supported, going back through PHP 8.2, 8.1, 8.0, 7.4, 7.3, 7.2 and 7.1, each with a Debian and an Alpine variant.
The legacy list is the part of the policy that pays off in practice, because a project still on an old PHP version does not have to build its own image, and the tag it pins will never be rebuilt underneath it.
The Dockerfile treats PHP version and flavour as arguments
The Dockerfile starts with three build arguments that determine what the image is: PHP_VERSION defaulting to 8.5, FLAVOUR defaulting to alpine, and TOOLBOX_VERSION pinned to a specific release, along with an exclusion tag for the tools that are not compatible with the chosen PHP version.
From there it is a conventional multi-stage build with one variant per base distribution. The Debian path starts from the php CLI image and installs the library and tool dependencies from apt, and the Alpine path starts from the Alpine PHP image and installs its equivalents from apk. The two are then joined into a single base stage, and the builder stages add the compilation dependencies, with the Debian and Alpine library lists differing in the same way as the runtime lists.
Two details are worth noting for anyone who maintains this kind of image. The apt and apk cache mounts are keyed by the target architecture, with a shared lock, so a multi-architecture build does not corrupt the cache of another architecture. And the comment on the Debian stage explains that the docker-clean configuration file is removed first, which is what enables apt caching at all, a small but classic piece of Docker knowledge that is easy to lose and expensive to rediscover.
The result is that the same repository produces a Debian image and an Alpine image, and a different PHP version, from one Dockerfile. The Makefile drives that with buildx, passing the PHP version and the flavour as build arguments, and the alpine and debian build targets default to the jakzal/phpqa:alpine and jakzal/phpqa:latest tags respectively.
The tool catalogue, and the version gaps in it
The tool table in the readme is the substance of what the image gives you, and it is presented per PHP version with a column for 8.3, 8.4 and 8.5.
The list covers the ground you would expect from a PHP quality suite. There is Composer itself, and a set of Composer plugins: composer-bin-plugin for installing binary vendors in isolated locations, composer-lock-diff for checking what changed after a composer update, composer-normalize for normalising composer.json files, composer-require-checker for verifying that no unknown symbols are used in a package's sources, composer-unused for showing unused packages by scanning your code, and cyclonedx-php-composer for producing a Software Bill of Materials in the CycloneDX format.
Then there are the analysis tools. deptrac enforces dependency rules between software layers, which is the one on this list that catches a class of mistake no linter does, since it is about the architecture rather than the code. dephpend detects flaws in the architecture, churn discovers good candidates for refactoring, and deprecation-detector finds usages of deprecated code. Infection is an AST based mutation testing framework. ecs sets up and runs coding standard checks, codeception is a BDD styled testing framework, and behat helps to test business expectations. There is also box, a fast zero config application bundler for PHARs, gherkin-lint-php for linting feature files, and diffFilter, which applies QA tools to a single pull request.
One row in that table is a good example of why the per-version columns exist. composer-require-checker is marked as unavailable on PHP 8.3 and available on 8.4 and 8.5, while composer-require-checker-3 is available on all three. Two rows for the same tool, where one is the version that works everywhere, is the kind of detail the table exists to communicate, and a user pinning the wrong PHP flavour will otherwise discover it as a missing binary.
The release is a phar, a make target and a git tag
The release process is described in the Makefile, and it is a good example of automating the parts of a release that are usually done by hand.
The devkit target downloads the latest release of devkit.phar from the toolbox repository, using the GitHub API to find the download URL:
curl -s https://api.github.com/repos/jakzal/toolbox/releases/latest | grep "browser_download_url.*devkit.phar" | cut -d '"' -f 4 | xargs curl -Ls -o devkit && chmod +x devkitThe update-readme-tools target then uses it to regenerate the tool table in the README:
./devkit update:readme --readme README.mdThat is the whole reason the readme's tool table and tag list are trustworthy. They are generated from the toolbox, so they cannot drift from what the image actually contains.
The release target is built on three smaller targets. readme-release reads the current version out of the README itself, using a grep and a sed over the lines that contain the repository blob URL for a version, and next-patch-release increments the last component of that version with awk. The release target then evaluates the next version, updates the README, stages it, commits with the message Release v<version>, creates an annotated tag whose message is Tag v<version> and pushes to origin master.
So the version number, which appears in the Docker tags, is derived from the documentation and then written back into it. It is a slightly circular arrangement, and it is the kind of arrangement that only works because the README is generated in the first place. The clean target removes the devkit binary, and the build targets pass a Composer secret mount when one is configured, so that a private Composer registry can be used during a build.
Why a container for quality tools at all
The value of this image is not that it contains tools you cannot install yourself. Every tool in the list is available through Composer. The value is that it makes the set of tools a property of the environment rather than of each developer.
Without an image like this, a quality pipeline in PHP is usually assembled by asking each developer to run composer global require with a list of packages, which produces a set of tools at whatever versions that person happened to install. Then the pipeline runs on a CI machine that somebody configured once, differently again, and the report you get on the pull request depends on which machine produced it.
The image removes that variability. The version of PHP, the version of every tool, the base distribution and the extensions are all fixed by the tag, and the same tag gives the same result on a laptop and on a CI runner. For a team that has been arguing about whether a new tool release broke a build, that is a real saving.
There is a second benefit that is easy to overlook. The catalogue is curated by somebody. The toolbox repository has a maintainer who decides which tools belong in the image, and the readme's table makes the result reviewable, so a team can read a list of what will run on their code instead of discovering it from a CI log. The catalogue also includes a few tools that a team would not think to install, such as composer-lock-diff, composer-require-checker, cyclonedx-php-composer for SBOMs, and diffFilter for running the tools on a single pull request, and those are exactly the ones that are hard to justify adding one at a time.
Where a prebuilt image is the wrong tool
The first limit is that the catalogue is fixed. If your project needs a tool that is not in the image, you cannot add it without building your own image, which means going back to the thing the image was supposed to avoid. In practice that means forking the Dockerfile, keeping it in step with upstream, and re-implementing the build arguments and the cache mounts yourself. A project whose toolchain is genuinely different from the default should build its own image from the toolbox rather than trying to bend this one.
The second is the mismatch with your own dependency constraints. The image installs the tools with their own Composer requirements, which are not the requirements of your project. A tool that behaves differently under a different version of a library, and a tool that is stricter or looser than the version your project pins, will produce results that do not match what a developer sees locally with the project's own vendor directory. The image reduces variability between environments, and it introduces a second environment.
The third is the daily rebuild. The latest tag changes every day, which is a feature for a developer and a hazard for a pipeline. Pinning a mutable tag is how a build breaks on a Friday for no reason, and the readme's own tag policy is the remedy, so the risk is entirely in the choice of tag.
The fourth is the image size and the shell. A full catalogue of tools, Composer, Graphviz, git, GnuPG and a PHP runtime with the extensions each tool needs is a large image, and it is a Debian or Alpine userland with a lot in it. That is fine for CI and awkward for some restricted environments, and the Alpine variant exists for the teams that need a smaller base.
The fifth is that a tool which is wrong for your project is still a tool you have to disable. The image gives you the set, not the configuration, and the configuration is the part that decides whether a rule is right for your codebase. Whether the report is useful depends on the rules you write, not on the catalogue.
Editorial conclusion
phpqa fits a team that wants one agreed set of PHP quality tools without installing a dozen Composer packages on every machine and without arguing about versions, and the immutable tags make a CI pipeline reproducible. It does not fit a project that needs a tool the image does not carry, or one that wants the tools to match the exact Composer constraints in its own project. Before adopting it, pick a tag deliberately, because the latest tag is rebuilt daily and will change under you, and check the tool catalogue for the PHP version you target, since composer-require-checker is marked as unavailable on PHP 8.3 while the version 3 variant is available on all three. The tool list and the installer live in the jakzal/toolbox repository rather than here, so changes to the catalogue arrive as image updates. The licence is MIT, version 1.125.8 was published on 2026-10-12, and the last push was on the same day.
Frequently asked questions
What does the phpqa Docker image contain?
A catalogue of PHP static analysis and quality tools, including Composer and several Composer plugins such as composer-unused, composer-lock-diff, composer-normalize and cyclonedx-php-composer, plus deptrac, dephpend, churn, deprecation-detector, infection, ecs, codeception, behat, box and others. The list of tools and the installer are managed in the jakzal/toolbox repository, and the readme's table is generated from it.
Which PHP versions does phpqa support?
The current tags cover PHP 8.3, 8.4 and 8.5, each in a Debian and an Alpine flavour. Legacy tags are kept for PHP versions that are no longer supported, going back through 8.2, 8.1, 8.0, 7.4, 7.3, 7.2 and 7.1.
How often are the phpqa images updated?
The floating tags such as latest, debian, alpine and php8.5 are updated daily. The minor version tags such as 1.125 are updated when the patch version changes. The full version tags such as 1.125.8 are never updated, which makes them the ones to pin for a reproducible pipeline.
How is a new phpqa release cut?
The Makefile drives it. The devkit target downloads the latest devkit.phar from the toolbox repository, update-readme-tools regenerates the tool table in the README, and the release target derives the next patch version, updates the README, commits it, creates an annotated tag and pushes to origin master.
Can I build the phpqa image myself for another PHP version?
Yes. The Dockerfile takes PHP_VERSION, FLAVOUR and TOOLBOX_VERSION as build arguments, and the Makefile has build targets that pass the PHP version and the flavour to docker buildx, with separate Debian and Alpine targets.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jakzal-phpqa)Was this useful?