Skip to content

PHP images ​

A docker site runs in an app container, and the container runs an image. A new project gets one of the dqx-php images: PHP, Composer, Node and the tools a Laravel checkout needs, built and tested together.

yaml
sites:
  - domains: { primary: crm.northwind.local.example }
    image: dolphiq/dqx-php85-alpine-slim:1.0

The dqx-php images ​

The name says what is inside: dolphiq/dqx-php<major><minor>-<distro>-<flavour>:1.0.

PartValues
PHP8.2, 8.3, 8.4, 8.5
distroalpine (default), debian
flavourslim (default), complete

slim is the default. complete adds imagick, pdo_pgsql and pgsql to the extensions, and zsh with oh_my_zsh and zsh_autosuggestions to the tools. dqx image info <image> lists both, so you can compare before you choose.

The tag is :1.0, and it floats. Patches to PHP, Node and the tools arrive under the same tag; the distro and the Node major stay. dqx images --update pulls the newer image and dqx up recreates the containers on it. dqx never writes :1.0.0 (it would never move) or :1 (it would move to a new Node major).

New projects start on PHP 8.5, so dqx init writes dolphiq/dqx-php85-alpine-slim:1.0. PHP 7.4 and 8.1 have no dqx-php image: they keep dolphiq/php74-composer-npm:2 and dolphiq/php81-composer-npm:3.

An existing project keeps what it has. A site that names php: '8.3' and no image still resolves to dolphiq/php83-composer-npm:3; dqx does not change an image behind your back. dqx init --from an older .dq/config.yml with php and no image is a new project, though, and gets a dqx-php image. The next section and moving an existing project show how to choose the change.

Alpine, or Debian ​

Alpine is the default: a small image that builds fast. It uses musl, not glibc, and a few programs only ship for glibc. For those, use Debian.

Say it in .dq/config.yml, for the project (dqx 1.77+; it is only written when it is not alpine):

yaml
distro: debian

or when you create the project:

bash
dqx init --distro debian

Without --distro, dqx init (when it is given a target, --from or --domain) looks at the checkouts that exist and gives Debian to a site that needs glibc, and says why:

text
distro         debian  (needs glibc: spatie/browsershot, puppeteer)

The scan reads composer.lock (every package composer installs, so barryvdh/laravel-snappy brings in knplabs/snappy and spatie/laravel-pdf brings in spatie/browsershot; composer.json when there is no lock), the dependencies and devDependencies of package.json, and your own PHP sources, so a fresh clone with nothing installed is fine, and vendor and node_modules are skipped. On the npm side only puppeteer also counts when another package pulls it in (it downloads Chrome on install); @playwright/test brings playwright, but dqx runs that suite in its own browser service. It looks for:

  • composer: spatie/browsershot, knplabs/snappy, h4cc/wkhtmltopdf-amd64, h4cc/wkhtmltoimage-amd64, mikehaertl/phpwkhtmltopdf
  • npm: puppeteer, playwright, @sparticuz/chromium
  • your own PHP: a use of GLOB_BRACE, which musl does not have

--distro on a site with PHP 7.4 or 8.1 is ignored, with a message: those images have no other distro to pick.

Switching later ​

bash
dqx image switch --distro debian          # a dry run: what would change
dqx image switch --distro debian --write  # rewrite .dq/config.yml and run dqx sync
dqx up --build                            # build the new container

dqx image switch without --write only prints the plan. With it, dqx rewrites the image lines in .dq/config.yml (your comments stay), runs the sync, honours --runtime, and prints Rebuild with: dqx up --build. --site <domain> moves one site only. When every site is already on the distro, --write still records distro: for the project.

Without --distro, a site already on dqx-php keeps its distro. A site on an old house image goes to the project's distro (Alpine when it states none), or to Debian when its checkout needs glibc. A dqx-php image on a tag from before :1.0 (:0.1, :0.2) moves to :1.0 of the same image, distro and flavour kept; the proposals say <site> runs <image>; :1.0 is out. A site on PHP 7.4 or 8.1 is skipped (no dqx-php image for PHP 8.1) and the other sites are still moved.

When the libc changes, reinstall node_modules. Native packages (esbuild, rollup, sharp, the Tailwind engine) are built for one libc. After a move between Alpine and Debian, or from an old glibc image to Alpine, the old ones fail with Cannot find native binding. dqx says so when the switch crosses libc; run dqx npm ci (or your install command) after the rebuild.

dqx only moves images it knows. A site on an image of your own is refused with the reason, and nothing is changed.

What is inside ​

bash
dqx image info dolphiq/dqx-php85-alpine-slim:1.0

prints the image's PHP, Node, Composer, extensions and tools, read from the image's label: from the local image when this machine has it, otherwise from the registry without pulling. --json prints the same as JSON. What Docker Hub said about a tag is kept on disk for a day (under ~/.local/share/dqx/image-contents), so neither this nor the dashboard asks it every time; --refresh asks again, and dqx images --update renews the entry of each image it pulls.

Inside a project, dqx image info shows each app container: its image, what the image declares, and drift, when the running container is older than the image its tag points at now (an older pull of :1.0). dqx images --update, then dqx up, closes the gap.

In the container, the image states itself in /etc/dqx-image.json, read from the binaries when it was built, and dqx-image-info prints it.

In the dashboard, a project's app service chips say what their image holds: app, and each container dqx adds for another PHP version (app83). Click one, also on a dashboard started with --read-only, for the configured image, its PHP, Node, Composer and distro, the first eight extensions (+N more for the rest) and a warning when the container is older than its image. A site with phpExtensions runs an image dqx builds on this machine: the popover names the configured image and adds built locally with: <extensions>, and those extensions never count as drift. A dolphiq image links to its tag on Docker Hub; a local or foreign-registry image has no link. all image details opens an overlay with everything the label and the running container say: each extension with its version, the tools and packages, PHP, Node and Composer as the image declares them beside what runs, the drift lines, and where the label was read (the image on this machine, or Docker Hub, kept for a day). For an image without the label it says "no dqx contents label", for an image on another registry "image contents not checked (not on Docker Hub)", and while it loads "reading image...", or "image info unavailable" when that fails. Each open asks again, so a switched image shows without a reload.

Moving an existing project ​

dqx proposes the move where you already look:

  • dqx doctor adds a ! note in an images section
  • dqx adopt --write prints "Images the sites could move to:"
  • dqx images --outdated prints <path>: can move to dqx-php for each project

A proposal keeps the site's PHP version, and picks Debian when the glibc scan says so. A site already on a dqx-php Alpine image that needs glibc gets the Debian proposal too. Then:

bash
dqx image switch            # read the plan
dqx image switch --write
dqx up --build

Move one site first with --site, check it, then the rest.

Your own image ​

image: on a site takes any image. dqx does not change it, and it does not refuse it:

yaml
sites:
  - domains: { primary: crm.northwind.local.example }
    image: registry.northwind.local.example/crm-app:4

What dqx expects of it, from what it generates:

  • PHP-FPM on port 9000. nginx sends PHP to app:9000. The container's healthcheck runs php-fpm -t; that check is the dqx-php images' own, not something dqx adds.
  • php and composer on the PATH, because dqx exec, dqx composer and dqx php run them in the container, and a shell (sh) for the commands and hooks dqx runs there.
  • The checkout at /var/www. The stack mounts it there and sets it as the working directory.
  • Root's home for caches. The stack mounts the Composer cache and auth file under /root/.composer, and ~/.ssh/id_rsa as /root/.ssh/id_rsa_host. An image that runs as another user does not see them.
  • PHP configuration in /usr/local/etc/php/conf.d/, where dqx mounts php-ini-overrides.ini and, on images that are not dqx-php, the Xdebug ini.
  • If a site uses phpExtensions or packages, a package manager (apk, apt-get or dnf) for the image dqx builds on top of yours.

What you lose without a dqx-php image:

  • dqx image info has no label to read, so no contents and no drift check
  • the app chip's image popover in the dashboard has nothing to show
  • Xdebug: on the dqx-php images dqx mounts its settings ini at conf.d/zz-dqx-xdebug.ini and switches the mode through XDEBUG_MODE, which survives a recreated container. On any other image dqx copies an ini file into the container instead, which a recreated container loses until the next dqx xdebug
  • dqx image switch and the proposals leave the site alone

The better option is to start from a dqx-php image and add what you need:

dockerfile
FROM dolphiq/dqx-php84-alpine-slim:1.0
RUN apk add --no-cache ghostscript

You keep the labels, the info, the drift check and Xdebug, and add only your packages. Point the site at the result with image: (or compose.app.build).

For only a missing PHP extension, do not build anything. Declare phpExtensions and dqx writes the Dockerfile for you; see PHP extensions and packages.

dqx v1.78.0