Who this is for: people who run PHP applications on these images (developers and DevOps). Everything you need to choose an image, start a container and add PHP extensions is on this page. If you maintain the images, read the project documentation in the repository instead.
Ready-made PHP images for Wiedii projects. A Docker image is a packaged, ready-to-run environment. These ones contain PHP, a web server, the PHP extensions most applications need and safe defaults. They run as a non-root user (an ordinary account instead of the all-powerful root administrator), use base images pinned by digest (a fingerprint of their content), and come in a production mode and a development mode. There are versions for Laravel, Symfony and any other PHP application. Every tag works on Intel/AMD (linux/amd64) and on Apple Silicon or ARM servers (linux/arm64).
An image name is made of the PHP version, the variant and the operating system, plus -dev for the development mode:
wiediiregistry/php:<php-version>-<variant>-<debian|alpine>[-dev]
The version is always the full patch number (8.5.12, not 8.5). Examples: wiediiregistry/php:8.5.12-fpm-debian, wiediiregistry/php:8.5.12-symfony-alpine, wiediiregistry/php:8.5.12-fpm-alpine-dev. You can see every tag on this Docker Hub page, under Tags.
A variant is a flavor of the image for one kind of project:
| Variant | What it is | Use it for | Port |
|---|---|---|---|
fpm | PHP-FPM plus the Caddy web server | Generic web applications, APIs | 8080 |
laravel | Same as fpm, set up for Laravel (adds pcntl) | Laravel applications | 8080 |
symfony | Same as fpm, set up for Symfony (adds pcntl) | Symfony applications | 8080 |
cli | PHP on the command line, no web server | Queue workers, schedulers, cron | none |
bare-fpm | Like fpm, on a hardened base with no known flaws | Web apps that need the smallest attack surface | 8080 |
bare-cli | Like cli, on a hardened base with no known flaws | Workers that need the smallest attack surface | none |
PHP-FPM is the program that runs PHP code for a web server, and Caddy is that web server. Not sure which to pick? Use laravel, symfony or fpm for a web application and cli for background work.
bare-* variants have limitsThe bare-* variants use Docker Hardened Images (DHI): minimal base images from Docker with no known security flaws (zero CVEs, where a CVE is a publicly listed vulnerability). They trade convenience for that:
RUN commands in your final build stage or open bash in the running container. Do the work in an earlier build stage (for example from the matching -dev image) and copy the result with COPY --from.intl, mbstring, sodium, opcache, ctype, iconv, xml, pdo and pdo_sqlite) and none of the ones we add on top in the standard images (redis, mongodb, gd, imagick, zip, bcmath, apcu, igbinary, gmp, calendar, mysqli, pdo_mysql). If your application needs those, choose a standard variant.nonroot (UID 65532) instead of app.| Mode | Tag suffix | What is inside |
|---|---|---|
| Production | none | PHP, the web server (web variants), OPcache with JIT (keeps compiled PHP in memory), no debug tools, no Composer |
| Development | -dev | Everything above plus Xdebug (the PHP debugger, port 9003), Composer (PHP's package manager), Zsh, Starship (a friendly prompt), mise (installs other runtimes such as Node), git-flow-next and GPG for signing commits. The app user has sudo (runs a command as administrator), for local development only |
A -dev image is meant to be opened as a devcontainer, a ready-made workspace that VS Code or JetBrains starts inside Docker. The bare-* -dev images differ: their shell is bash (Debian) or ash (Alpine) instead of Zsh, they have no GPG and no sudo, and the only user is nonroot.
Put one of these in your project's Dockerfile, then build it with docker build -t my-app .. Each block below is a separate example, not one file.
Production, Laravel. The production image has no Composer, so it is borrowed from its official image during the build only (the @sha256 part is explained below):
FROM wiediiregistry/php:8.5.12-laravel-debian
COPY --chown=app:app . /var/www/html/
RUN --mount=type=bind,from=composer/composer:lts-bin@sha256:643030268f2b073331256d8dfaf8e935dfbb0164fc39c1d41e7355e26ad80707,source=/composer,target=/usr/bin/composer \
composer install --no-dev --optimize-autoloader --no-interaction
RUN php artisan optimize
Production, general PHP (no build step):
FROM wiediiregistry/php:8.5.12-fpm-debian
COPY --chown=app:app . /var/www/html/
Development, Laravel (debugger, Composer, Zsh and mise included):
FROM wiediiregistry/php:8.5.12-laravel-debian-dev
Command-line worker:
FROM wiediiregistry/php:8.5.12-cli-debian
COPY --chown=app:app . /var/www/html/
CMD ["php", "artisan", "queue:work", "--sleep=3", "--tries=3"]
Check that it works. First, ask the image for its PHP version:
docker run --rm wiediiregistry/php:8.5.12-fpm-debian php -v
It prints the PHP version. Next, start the image you built from one of the examples above. The web variants listen on port 8080. This command keeps running, so leave it open:
docker run --rm -p 8080:8080 my-app
In a second terminal window, call the health check. HTTP/1.1 200 in the answer means the web server is up:
curl -i http://localhost:8080/healthz
@sha256:… in the Composer line islts-bin is a label. The Composer maintainers move it to a newer release whenever they publish one, so building today and next month can give you a different Composer without any change in your Dockerfile.composer/composer:lts-bin@sha256:… always means those exact bytes, and Docker refuses anything else.Dockerfile gives the same result next year, and a changed or tampered image cannot slip into your build unnoticed. Docker uses the digest; the tag stays so a person can read what it is.To get the current digest:
docker buildx imagetools inspect composer/composer:lts-bin.Digest: line, the one at the top (for example sha256:6430…0707). Ignore the digests further down under Manifests:: they belong to single CPU types.@ in your Dockerfile.The digest in the example is the one these images used when this text was written, so it may not be the newest. Update it on purpose, when you want a newer Composer: repeat the three steps, rebuild and run your tests. Dependency bots such as Renovate can propose the update for you if they recognize --mount … from= lines.
A PHP extension adds a capability to PHP, such as talking to a database. All standard variants (fpm, laravel, symfony, cli) ship these 18 extensions in production and development:
zip, sodium, mbstring, gd, xml, calendar, imagick, opcache, apcu, igbinary, gmp, intl, bcmath, redis, mongodb, mysqli, pdo, pdo_mysql.
On top of that: pcntl in laravel and symfony (Laravel Horizon and Symfony Messenger workers need it), and Xdebug in every -dev image. ctype and iconv are compiled into PHP in every image. Bare images carry only what Docker compiles in (see above).
Use install-php-extensions. It builds each extension from a verified source, and it never uses pecl (PHP's old extension tool, which is deprecated and had outages). The -dev images already contain the command: inside a running standard -dev container, run sudo install-php-extensions pcov (the bare-* -dev images have no sudo). In a production Dockerfile, mount the installer image for the one RUN that needs it, so none of it stays in your final image:
FROM wiediiregistry/php:8.5.12-laravel-debian
USER root
RUN --mount=type=bind,from=wiediiregistry/php-extension-installer:0.1.3@sha256:e03f134a49496945dbb72c9da46e99da82ae22db4f61355c577afaebef1f8975,source=/usr,target=/opt/wiedii-ext \
sh /opt/wiedii-ext/bin/install-php-extensions exif pcov
USER app
0.1.3 is the installer release these images are built with, and the sha256: part is its digest (a fingerprint of the image's exact content), so your build gets exactly the same installer. For a newer release, run docker buildx imagetools inspect wiediiregistry/php-extension-installer:<release> and copy the first Digest: line. Avoid the short tags 0.1 and 0: they move to each new release. The installer has its own page on Docker Hub: wiediiregistry/php-extension-installer.
What to know:
Names outside our tested list are looked up on Packagist, the official index of PIE (the PHP Foundation's extension installer), and installed at the latest stable release. The same applies to a version you ask for, such as redis-6.3.0. That path is not reproducible (a newer release can appear tomorrow), so the command prints a warning. It is a deliberate exception: to get a reproducible build, ask the maintainers to add the extension to the tested list with a pin.
Bare images: no extra PHP-own extensions such as gd or zip, because there is no PHP source in them. On bare Debian, none that needs system libraries either; bare Alpine can install those.
Bare production images have no shell, so you cannot run the installer in them: build the extension in the matching -dev image and copy only its .so file (the compiled extension) and its .ini file (the setting that turns it on).
Rate limits: extensions are downloaded from GitHub, which limits anonymous requests. If a build hits the limit, give it a GitHub token as a BuildKit secret (a value that exists only while one build step runs). Never pass it as a build argument: arguments stay readable in the image history. Add --mount=type=secret,id=composer_auth to the RUN line above, put the token in a file, and pass the file to the build:
echo '{"github-oauth": {"github.com": "<token>"}}' > auth.json
docker build --secret id=composer_auth,src=auth.json .
Replace <token> with a GitHub token you created, and never commit auth.json.
app in every standard image and nonroot in every bare-* image. Linux identifies each user by a number, the UID (user ID), and each group by a GID (group ID): app is UID/GID 1001 and nonroot is UID/GID 65532. In production images no process runs as root, so a compromised application does not get administrator rights in the container. If a mounted folder is not writable, make the host folder match that UID/GID. app belongs to its own group only: it is no longer in the www-data group (GID 33 on Debian, 82 on Alpine), so a project that relied on www-data group permissions must switch to app:app.sudo in -dev images only: a -dev image is a local development workspace, and the maintainers allow developers to add tools inside it without rebuilding (mise use, sudo apt-get install, sudo apk add, sudo install-php-extensions). For that, app has passwordless sudo there; the bare-* -dev images have none. It is a deliberate exception, approved by the tech lead on 2026-10-25; the maintainers will review it by 2027-04-24. Production images have no sudo. Never run a -dev image in production.expose_php=Off, safe sessions, only .php files executed. Caddy sends security headers (HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy and others) and blocks sensitive paths (.env, .git, vendor, storage/logs). Caddy compresses responses with Brotli, zstd and gzip.-dev images of the bare-* variants contain a GitHub token in Composer's configuration (auth.json), so composer install is not throttled by GitHub's limit for anonymous requests (60 per hour per IP address). This helps when your own Dockerfile installs packages in a build stage that starts from one of these images, and when you run Composer inside a -dev container. It belongs to a dedicated company account that can only read public repositories, so it gives no access to anything private. It is kept on purpose. Anyone who pulls the image can read it, and security scanners such as Docker Scout or Trivy may report it; that finding is expected. The bare-* production images do not contain it.GET /healthz answers 200 when the web server is up). PHP-FPM listens on 9000 inside the container only; do not publish it. The cli and bare-cli variants open no port.no match for platform: the tag has no image for your CPU type. Check that the tag lists both linux/amd64 and linux/arm64: docker buildx imagetools inspect wiediiregistry/php:<tag>.app (UID 1001), or nonroot (UID 65532) on bare images. Make the host folder match.docker exec -it <container> bash opens a shell (not on bare-* production images, where docker debug <container> works instead; it comes with Docker Desktop and is missing from some Docker installations, so otherwise run the matching -dev image), and docker logs <container> shows its output.The project repository has longer guides (variants, tags, the production and development environments, security, every extension by variant) in English and Spanish, in its docs/ folder. It is internal to Wiedii, so this page repeats everything a visitor needs. General references: Docker documentation, PHP manual, Caddy documentation.
UNLICENSED. Internal Wiedii use only.
Content type
Image
Digest
sha256:2320628cc…
Size
396.4 MB
Last updated
7 days ago
docker pull wiediiregistry/php:8.5.12-symfony-alpine-dev-6f8cf4bcf5ed