Skip to content

phpMyAdmin Docker Compose Stack

phpMyAdmin as a php-fpm image plus the nginx in front of it, rebuilt daily so the base underneath the application stays current.

Why this exists

The upstream phpmyadmin/phpmyadmin image is rebuilt when phpMyAdmin itself is released, not when its base picks up fixes. Measured on 2026-08-08, with trivy against the published images:

image size findings with a fix CRITICAL+HIGH with a fix
phpmyadmin/phpmyadmin:5.2.3 (built 2025-10-08) 708 MB 4623 3396 562
phpmyadmin-php-fpm (pinned) 157 MB 18 18 10
phpmyadmin-php-fpm (rolling) 157 MB 0 0 0

The application code is the same in all three. What differs is the age of the Debian base — and, for the rolling variant, of phpMyAdmin's own bundled PHP libraries. :latest upstream is byte-identical to :5.2.3, so waiting for an upstream rebuild is not a plan.

Two variants

Pinned (default) ships the libraries exactly as the phpMyAdmin release did. Reproducible, and identical to upstream in everything but the base image.

Rolling (ROLLING_DEPS=true) refreshes those libraries within the constraints phpMyAdmin's own composer.json declares — twig/twig moves from the released 3.11.3 to 3.28.0, for instance. That is where the last 18 findings go. It is a deliberate deviation from the released tarball, so it is opt-in.

Quick start

cp .env.example .env          # set MARIADB_ROOT_PASSWORD for the demo database
make up                       # app + web + a MariaDB to look at

phpMyAdmin then answers on http://localhost:8080.

Against a database you already run, leave the demo out and point at it:

PMA_HOST=your-db-host docker compose up -d app-assets app web

Two images

Image What it is
ghcr.io/netresearch/phpmyadmin-php-fpm php-fpm on 9000, serves no HTTP
ghcr.io/netresearch/phpmyadmin-nginx nginx on 8080 with the configuration and the document root baked in, running as an unprivileged user

Deploy both at the same tag. They share the document root, which is copied from one build stage into both, so mixing tags means running two phpMyAdmin versions against each other. Both are built in the same workflow run and carry the same tag set for that reason.

Two containers, no mounted files:

services:
  pma-fpm:
    image: ghcr.io/netresearch/phpmyadmin-php-fpm:latest
    environment:
      PMA_HOST: db
  pma:
    image: ghcr.io/netresearch/phpmyadmin-nginx:latest
    environment:
      PMA_FPM_UPSTREAM: pma-fpm:9000
    ports:
      - "8080:8080"

Using only the php-fpm image

Anyone who already runs a web server can take the php-fpm image alone. It listens on 9000, and the web server needs the application tree to serve static files, which the app-assets service copies into a shared volume on every start. See docs/using-the-image-standalone.md.

Configuration

Everything is environment-driven and the variable names match the upstream image, so an existing deployment can switch without rewriting its environment. .env.example documents each one. Two are worth calling out:

  • PMA_BLOWFISH_SECRET — exactly 32 characters, encrypts the auth cookie. Without it the container generates one into its tmp volume and warns; lose that volume and everyone is logged out.
  • PMA_ABSOLUTE_URI — needed when phpMyAdmin is served under a path or a public hostname the container itself never sees.

A config.inc.php you mount yourself is left alone; one this image generated is rewritten on each start, so a changed environment takes effect.

What the image does not contain

setup/, examples/ and test/ are removed. The setup wizard writes configuration through the browser, which in a container is both pointless — the configuration comes from the environment — and a way in.

/ping and /status are php-fpm's own endpoints. The shipped nginx config answers them on loopback only: in Docker every request through a published port arrives from the bridge gateway, so allowing "the private ranges" would publish them to anyone who can reach the port.

Maintenance

make lint runs hadolint and a compose validation, make test builds and runs the container-structure tests, make scan runs trivy against the built image. The daily CI rebuild is what keeps the promise in the table above true; Renovate bumps the base versions and the release pin.

Licence

MIT for everything in this repository (see LICENSE-MIT), documentation under CC-BY-SA-4.0. phpMyAdmin itself is GPL-2.0-only and is downloaded at build time, not vendored here.

About

Opinionated phpMyAdmin stack — daily-rebuilt PHP 8.4 / Alpine php-fpm image plus the nginx in front of it. Upstream's image carried 3396 fixable CVEs; this one carries none in its OS layer.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages