PHP-FPM on top of the official PHP image, with the webdevops entrypoint, supervisord, cron, syslog-ng and the application user. The -dev variant adds Xdebug, the Blackfire extension, Postfix and an SSH server for local development.
webdevops/php-official is built from the same Dockerfiles as webdevops/php.
| Tags | Based on |
|---|---|
8.2, 8.3, 8.4, 8.5 |
php:<version>-fpm-bookworm (Debian 12) |
8.2-alpine, 8.3-alpine, 8.4-alpine, 8.5-alpine |
php:<version>-fpm-alpine |
Both webdevops/php and webdevops/php-dev use these tags. For the differences between the Debian and the Alpine tags, see Debian or Alpine.
Always use one of these tags: an image without a tag (latest) and the alpine tag are old builds from 2019 and 2020, see Known issues.
services:
php:
image: webdevops/php:8.5
volumes:
- ./:/appPHP-FPM listens on port 9000. For a web server in the same container use php-nginx or php-apache.
Started by default: php-fpm, cron, syslog. The -dev variant also starts postfix and ssh. See Customization. PHP-FPM listens on port 9000 of all IPv6 and IPv4 addresses; if the container has no IPv6, of all IPv4 addresses.
| Setting | Value |
|---|---|
memory_limit |
512M |
max_execution_time |
300 |
post_max_size, upload_max_filesize |
50M |
max_input_vars |
5000 |
date.timezone |
UTC |
display_errors |
0 |
session.upload_progress.enabled |
0 |
| OPcache | enabled, opcache.memory_consumption 256, opcache.revalidate_freq 2 |
All of these can be changed with environment variables, see PHP. For larger uploads and long requests, see Uploads and long requests; for production, see Production.
All PHP images disable session.upload_progress.enabled by default. Session upload progress lets PHP write request-controlled data into the session while a multipart upload is parsed, before application code runs. Regular file uploads ($_FILES) and sessions are not affected. To turn it back on, set php.session.upload_progress.enabled: "1".
Applications that read the upload progress from the session, using a form field named like session.upload_progress.name (default PHP_SESSION_UPLOAD_PROGRESS), need this setting turned on. That includes cancelling an upload through the session (cancel_upload). Calling ini_set() in the application does not help: it returns false and the value stays off. Progress bars that measure the upload in the browser are not affected.
All tags contain these extensions:
amqp, apcu, bcmath, bz2, calendar, ctype, curl, date, dom, excimer, exif, ffi, fileinfo, filter, ftp, gd, gettext, gmp, hash, iconv, imagick, intl, json, ldap, libxml, mbstring, memcached, mongodb, mysqli, mysqlnd, openssl, opentelemetry, pcntl, pcre, pdo, pdo_mysql, pdo_pgsql, pdo_sqlite, pgsql, phar, posix, protobuf, random, readline, redis, reflection, session, shmop, simplexml, soap, sockets, sodium, spl, sqlite3, standard, sysvmsg, sysvsem, sysvshm, tokenizer, vips, xml, xmlreader, xmlrpc, xmlwriter, xsl, yaml, Zend OPcache, zip, zlib
Differences between tags:
| Extension | Tags |
|---|---|
imap |
8.2, 8.3, 8.2-alpine, 8.3-alpine |
| ionCube Loader | Debian-based tags (8.2 to 8.5) |
lexbor, uri |
8.5, 8.5-alpine |
Disable extensions you do not need with PHP_DISMOD, for example PHP_DISMOD: redis,apcu.
docker-php-ext-install from the official PHP image still works for extensions that come with PHP's source code, for example dba. On Alpine it installs the build tools it needs and removes them again.
FROM webdevops/php:8.5
RUN docker-php-ext-install dbaFor extensions from PECL, use PIE. It does not have to stay in the image; mount it for the RUN that needs it. PIE enables the extension for PHP-FPM and the PHP CLI. The Debian images contain the build tools:
FROM webdevops/php:8.5
RUN --mount=type=bind,from=ghcr.io/php/pie:bin,source=/pie,target=/usr/local/bin/pie \
pie install pecl/pcovOn Alpine, install the build tools ($PHPIZE_DEPS) in the same RUN and remove them afterwards:
FROM webdevops/php:8.5-alpine
RUN --mount=type=bind,from=ghcr.io/php/pie:bin,source=/pie,target=/usr/local/bin/pie \
apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \
&& pie install pecl/pcov \
&& apk del .build-depsComposer 2 is installed as composer2, Composer 1 as composer1. composer links to one of them: Composer 2 by default, Composer 1 with COMPOSER_VERSION: "1".
COMPOSER_VERSION switches the link when a container starts, not during docker build. In a Dockerfile, composer is always Composer 2; call composer1 or composer2 directly:
FROM webdevops/php:8.5
COPY . /app
RUN cd /app && composer2 install --no-interactionEvery PHP version comes as a Debian tag (8.5) and an Alpine tag (8.5-alpine). This applies to all php-* images. The differences:
| Topic | Debian | Alpine |
|---|---|---|
| Base image | php:<version>-fpm-bookworm (Debian 12) |
php:<version>-fpm-alpine |
Download size of webdevops/php:8.5 |
about 430 MB | about 170 MB |
| ionCube Loader | included | not included |
imap extension |
8.2 and 8.3 only |
8.2-alpine and 8.3-alpine only |
| Build tools for extensions | included | installed by docker-php-ext-install; for PIE install $PHPIZE_DEPS |
| Locales | several hundred, for example de_DE.utf8 |
none |
| Installing packages | apt-install |
apk-install |
docker-cronjob |
works | does not work, see Known issues |
docker-image-cleanup |
works | fails, see Known issues |
Apache index files in php-apache |
index.html, index.cgi, index.pl, index.php, index.xhtml, index.htm come before WEB_DOCUMENT_INDEX |
index.html comes before WEB_DOCUMENT_INDEX |
mail() without Postfix |
fails: there is no sendmail command |
fails: BusyBox sendmail finds no mail server |
Use the images without -dev in production. The -dev images load Xdebug and start Postfix and an SSH server, and php-dev sets the password dev for root and application.
Check these defaults for your application:
- PHP-FPM processes:
pm.max_childrenis5(FPM_PM_MAX_CHILDREN). PHP-FPM handles at most five requests at the same time; further requests wait. When that happens, the container log showsserver reached pm.max_children setting (5), consider raising it. Raise the value if the container has memory to spare; each process can use up tomemory_limit(512M). If the log showsseems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), PHP-FPM starts processes too slowly for the load; raiseFPM_PM_START_SERVERSand the spare server settings, see PHP-FPM. - Code in the image: if your Dockerfile copies the application code into the image, set
PHP_OPCACHE_VALIDATE_TIMESTAMPS: "0". PHP then no longer checks the files for changes; a changed file takes effect only after PHP-FPM restarts. Do not use it with code mounted from the host. - Errors:
display_errorsis0(PHP_DISPLAY_ERRORS), so error messages are not shown to visitors. Keep it that way. - Uploads and long requests: the limits are in Uploads and long requests.
services:
app:
build: .
ports:
- "8080:80"
environment:
FPM_PM_MAX_CHILDREN: "20"
PHP_OPCACHE_VALIDATE_TIMESTAMPS: "0"Check the values in the running container. php-fpm -tt prints the PHP-FPM configuration. php-fpm -i prints the PHP settings that PHP-FPM uses; php -i shows the values of the PHP CLI, which differ, for example max_execution_time is 0 there.
docker compose exec app php-fpm -tt
docker compose exec app php-fpm -iPHP's mail() passes each message to /usr/sbin/sendmail.
In the images without -dev, Postfix is not running and mail() returns false. The Debian images have no /usr/sbin/sendmail; in the Alpine images it is BusyBox's sendmail, which finds no mail server on 127.0.0.1. To send mail with mail(), enable Postfix in your Dockerfile and set POSTFIX_RELAYHOST to a mail server that accepts mail from the container. There is no variable for a login at the relay host.
FROM webdevops/php-nginx:8.5
RUN docker-service enable postfixservices:
app:
build: .
environment:
POSTFIX_RELAYHOST: "[mail.example.com]:25"The -dev images start Postfix. It has no relay host and delivers each message directly to the recipient's mail server.
Warning
Without a relay host, Postfix in a development container tries to deliver mail to real recipients. Send it to a mail catcher such as Mailpit instead.
services:
app:
image: webdevops/php-nginx-dev:8.5
ports:
- "8080:80"
volumes:
- ./:/app
environment:
POSTFIX_RELAYHOST: "[mailpit]:1025"
mailpit:
image: axllent/mailpit
ports:
- "8025:8025"Mail from PHP-FPM and from the PHP CLI then shows up in Mailpit at http://localhost:8025.
php-dev is built on php and changes the following:
- Xdebug is loaded by default (
xdebug.mode=debug,xdebug.start_with_request=trigger,xdebug.client_port=9003), see Xdebug with an IDE.PHP_DEBUGGERswitches to Blackfire or turns both off, see Development images. Known issues: theXDEBUG_*variables are written into your ownphp.ini, and the Blackfire agent is missing. opcache.revalidate_freqis0, so changed files are picked up immediately.- Postfix and an SSH server are started. Postfix sends mail to real recipients unless you set a relay host, see Mail.
rootandapplicationhave the passworddev. Use the-devimages for local development only.
Xdebug connects to your IDE on port 9003. Set php.xdebug.client_host to the host the IDE runs on. host.docker.internal is the Docker host; Docker Desktop provides the name, on Linux add it with extra_hosts:
services:
app:
image: webdevops/php-nginx-dev:8.5
ports:
- "8080:80"
volumes:
- ./:/app
environment:
php.xdebug.client_host: host.docker.internal
extra_hosts:
- "host.docker.internal:host-gateway"Use php.xdebug.client_host, not XDEBUG_CLIENT_HOST: the XDEBUG_* variables are written into your own php.ini, see Known issues.
-
Start a debug session: Xdebug only connects when the request carries the trigger
XDEBUG_SESSION. In the browser, add?XDEBUG_SESSION=1to the URL, or set the cookieXDEBUG_SESSIONas browser extensions for Xdebug do. On the command line, set the environment variable:docker compose exec -u application -e XDEBUG_SESSION=1 app php bin/console app:import -
Path mapping: Xdebug reports the paths inside the container, for example
/app/public/index.php. In the IDE, map your project directory to/app. -
Client discovery:
xdebug.discover_client_hostis1. For browser requests Xdebug first connects to the address in theX-Forwarded-Forheader or, without it, to the address the request came from. Only if that fails does it usexdebug.client_host. On the command line there is no such address, and Xdebug always usesxdebug.client_host. That is why the example sets it.