Converting wordpress/mpm_prefork/php/apache2 to wordpress/mpm_event/php-fpm/apache2

I am trying to convert an Wordpress-PHP-Apache2 (mpm_prefork) server to Wordpress-PHPFPM-Apache2 (mpm_event) server. The current docker environment has become much slower with increased concurrent access to Wordpress.

Current environment: Ubuntu 24.04.4, docker 28.1.1, WP 6.9.4, php 8.4, apache2 2.4, mariadb 10.11

docker-compose.yml:

volumes:
  wordpress:
    external: true
  wplogs:
    external: true
  mariadb_wp2:
    external: true
  mysql_tmp:
    external: true
  mysql_run:
    external: true

services:
  db:
    image: mariadb-chmod
    restart: always
    command: [
        '--lower_case_table_names=1',
        '--transaction-isolation=READ-COMMITTED',
        '--autocommit=1'
    ]
    environment:
      MYSQL_DATABASE:             bitnami_wordpress
      MYSQL_USER:                 bn_wordpress
      MYSQL_PASSWORD:             *********
      MYSQL_ROOT_USER:            root
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-u", "bn_wordpress", "--password=*********"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - mariadb_wp2:/var/lib/mysql
      - mysql_tmp:/tmp
      - mysql_run:/run/mysqld

  wordpress:
    image: wp-php-apache
    restart: always
    ports:
      - 443:443
    dns:
      - 81.130.111.248
      - 81.130.111.249
    depends_on: [ db ]
    environment:
      WORDPRESS_DB_HOST:          db
      WORDPRESS_DB_USER:          bn_wordpress
      WORDPRESS_DB_PASSWORD:      *********
      WORDPRESS_DB_NAME:          bitnami_wordpress
      WORDPRESS_AUTH_KEY:         ***************
      WORDPRESS_SECURE_AUTH_KEY:  ***************
      WORDPRESS_LOGGED_IN_KEY:    ***************
      WORDPRESS_NONCE_KEY:        ***************
      WORDPRESS_AUTH_SALT:        ***************
      WORDPRESS_SECURE_AUTH_SALT: ***************
      WORDPRESS_LOGGED_IN_SALT:   ***************
      WORDPRESS_NONCE_SALT:       ***************
      WORDPRESS_CONFIG_EXTRA: |
        define( 'FORCE_SSL_ADMIN', true );
        define( 'WP_TEMP_DIR', dirname(__FILE__) . '/wp-content/temp/' );
    healthcheck:
      test: ["CMD", "/usr/bin/healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 5
    volumes:
      - wordpress:/var/www/html
      - wplogs:/var/log/apache2
      - ./wp_000-default.conf:/etc/apache2/sites-available/000-default.conf:ro
      - ./default-ssl.conf:/etc/apache2/sites-available/default-ssl.conf:ro
      - ./mpm_prefork.conf:/etc/apache2/mpm_prefork.conf:ro
      - ./mpm_worker.conf:/etc/apache2/mpm_worker.conf:ro
      - ./mpm_event.conf:/etc/apache2/mpm_event.conf:ro
      - ./wp_ports.conf:/etc/apache2/ports.conf:ro
      - ./docker-entrypoint.sh:/usr/local/bin/docker-entrypoint.sh:ro
      - /etc/lego/certificates/hollandnumerics.org.uk.crt:/etc/ssl/certs/hollandnumerics.org.uk.crt:ro
      - /etc/lego/certificates/hollandnumerics.org.uk.key:/etc/ssl/certs/hollandnumerics.org.uk.key:ro

mpm_prefork.conf:

StartServers              20
MaxRequestWorkers         40
MinSpareServers           1
MaxSpareServers           40
MaxConnectionsPerChild    0
ServerLimit               40
MaxClients                40

docker-entrypoint.sh:

#!/usr/bin/env bash
set -Eeuo pipefail

# Apache gets grumpy about PID files pre-existing
rm -f /var/log/apache2/httpd.pid

if [[ "$1" == apache2* ]] || [ "$1" = 'php-fpm' ]; then
	uid="$(id -u)"
	gid="$(id -g)"
	if [ "$uid" = '0' ]; then
		case "$1" in
			apache2*)
				user="${APACHE_RUN_USER:-www-data}"
				group="${APACHE_RUN_GROUP:-www-data}"

				# strip off any '#' symbol ('#1000' is valid syntax for Apache)
				pound='#'
				user="${user#$pound}"
				group="${group#$pound}"
				;;
			*) # php-fpm
				user='www-data'
				group='www-data'
				;;
		esac
	else
		user="$uid"
		group="$gid"
	fi

	if [ ! -e index.php ] && [ ! -e wp-includes/version.php ]; then
		# if the directory exists and WordPress doesn't appear to be installed AND the permissions of it are root:root, let's chown it (likely a Docker-created directory)
		if [ "$uid" = '0' ] && [ "$(stat -c '%u:%g' .)" = '0:0' ]; then
			chown "$user:$group" .
		fi

		echo >&2 "WordPress not found in $PWD - copying now..."
		if [ -n "$(find -mindepth 1 -maxdepth 1 -not -name wp-content)" ]; then
			echo >&2 "WARNING: $PWD is not empty! (copying anyhow)"
		fi
		sourceTarArgs=(
			--create
			--file -
			--directory /usr/src/wordpress
			--owner "$user" --group "$group"
		)
		targetTarArgs=(
			--extract
			--file -
		)
		if [ "$uid" != '0' ]; then
			# avoid "tar: .: Cannot utime: Operation not permitted" and "tar: .: Cannot change mode to rwxr-xr-x: Operation not permitted"
			targetTarArgs+=( --no-overwrite-dir )
		fi
		# loop over "pluggable" content in the source, and if it already exists in the destination, skip it
		# https://github.com/docker-library/wordpress/issues/506 ("wp-content" persisted, "akismet" updated, WordPress container restarted/recreated, "akismet" downgraded)
		for contentPath in \
			/usr/src/wordpress/.htaccess \
			/usr/src/wordpress/wp-content/*/*/ \
		; do
			contentPath="${contentPath%/}"
			[ -e "$contentPath" ] || continue
			contentPath="${contentPath#/usr/src/wordpress/}" # "wp-content/plugins/akismet", etc.
			if [ -e "$PWD/$contentPath" ]; then
				echo >&2 "WARNING: '$PWD/$contentPath' exists! (not copying the WordPress version)"
				sourceTarArgs+=( --exclude "./$contentPath" )
			fi
		done
		tar "${sourceTarArgs[@]}" . | tar "${targetTarArgs[@]}"
		echo >&2 "Complete! WordPress has been successfully copied to $PWD"
	fi

	wpEnvs=( "${!WORDPRESS_@}" )
	if [ ! -s wp-config.php ] && [ "${#wpEnvs[@]}" -gt 0 ]; then
		for wpConfigDocker in \
			wp-config-docker.php \
			/usr/src/wordpress/wp-config-docker.php \
		; do
			if [ -s "$wpConfigDocker" ]; then
				echo >&2 "No 'wp-config.php' found in $PWD, but 'WORDPRESS_...' variables supplied; copying '$wpConfigDocker' (${wpEnvs[*]})"
				# using "awk" to replace all instances of "put your unique phrase here" with a properly unique string (for AUTH_KEY and friends to have safe defaults if they aren't specified with environment variables)
				awk '
					/put your unique phrase here/ {
						cmd = "head -c1m /dev/urandom | sha1sum | cut -d\\  -f1"
						cmd | getline str
						close(cmd)
						gsub("put your unique phrase here", str)
					}
					{ print }
				' "$wpConfigDocker" > wp-config.php
				if [ "$uid" = '0' ]; then
					# attempt to ensure that wp-config.php is owned by the run user
					# could be on a filesystem that doesn't allow chown (like some NFS setups)
					chown "$user:$group" wp-config.php || true
				fi
				break
			fi
		done
	fi
fi

a2enmod ssl
a2ensite default-ssl.conf
service apache2 restart
service apache2 stop

exec "$@"

wp-php-apache Dockerfile:

FROM wordpress:6.9.4-php8.4-apache

mariadb Dockerfile:

FROM mariadb:10.11
RUN chmod 1777 /tmp

What changes should be made to use php-fpm and mpm_event instead?

Thanks in advance…Phil

It took time to realize what your question was, but I think I get it now. So you should not use a container as a virtual machine. You either use the wordpress image that contains apache httpd and PHP as a module or use the PHP FPM variant of the wordpress image and use another HTTPD container container to connect to the PHP FPM container.

I have an old, currently not really maintained httpd image that you can check to get an idea. It was designed to be used with a PHP FPM container.

GitHub repo for the source code

I did not change the MPM settings, but that would be the same as without containers

Do you have a specific issue we can help you with and you could not solve?

Hi @rimelek,

I’ve tried to incorporate your httpd container into my server:

docker-compose.yml:

volumes:
  wordpress:
    external: true
  wplogs:
    external: true
  mariadb_wp2:
    external: true
  mysql_tmp:
    external: true
  mysql_run:
    external: true

services:
  db:
    image: mariadb-chmod
    restart: always
    command: [
        '--lower_case_table_names=1',
        '--transaction-isolation=READ-COMMITTED',
        '--autocommit=1'
    ]
    environment:
      MYSQL_DATABASE:             bitnami_wordpress
      MYSQL_USER:                 bn_wordpress
      MYSQL_PASSWORD:             *********
      MYSQL_ROOT_USER:            root
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-u", "bn_wordpress", "--password=*********"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - mariadb_wp2:/var/lib/mysql
      - mysql_tmp:/tmp
      - mysql_run:/run/mysqld

  wordpress:
    image: wp-php-fpm
    restart: always
    ports:
      - 9000:9000
    dns:
      - 81.130.111.248
      - 81.130.111.249
    depends_on: [ db ]
    environment:
      WORDPRESS_DB_HOST:          db
      WORDPRESS_DB_USER:          bn_wordpress
      WORDPRESS_DB_PASSWORD:      *********
      WORDPRESS_DB_NAME:          bitnami_wordpress
      WORDPRESS_AUTH_KEY:         ******************
      WORDPRESS_SECURE_AUTH_KEY:  ******************
      WORDPRESS_LOGGED_IN_KEY:    ******************
      WORDPRESS_NONCE_KEY:        ******************
      WORDPRESS_AUTH_SALT:        ******************
      WORDPRESS_SECURE_AUTH_SALT: ******************
      WORDPRESS_LOGGED_IN_SALT:   ******************
      WORDPRESS_NONCE_SALT:       ******************
      WORDPRESS_CONFIG_EXTRA: |
        define( 'FORCE_SSL_ADMIN', true );
        define( 'WP_TEMP_DIR', dirname(__FILE__) . '/var/www/html/wp-content/temp/' );
    healthcheck:
      test: ["CMD", "curl", "-k", "https://localhost/wp-login.php"]
      interval: 30s
      timeout: 10s
      retries: 5
    volumes:
      - wordpress:/var/www/html
      - ./www.conf:/usr/local/etc/php-fpm.d/www.conf:ro
      - ./docker-entrypoint.sh:/usr/local/bin/docker-entrypoint.sh:ro

  web:
    image: webserver
    restart: always
    ports:
      - 443:443
    dns:
      - 81.130.111.248
      - 81.130.111.249
    depends_on: [ wordpress ]
    volumes:
      - wordpress:/var/www/html:ro
      - wplogs:/var/log/apache2
      - ./wp_000-default.conf:/usr/local/apache2/sites-available/000-default.conf:ro
      - ./default-ssl.conf:/usr/local/apache2/sites-available/default-ssl.conf:ro
      - ./mpm_prefork.conf:/usr/local/apache2/mpm_prefork.conf:ro
      - ./mpm_worker.conf:/usr/local/apache2/mpm_worker.conf:ro
      - ./mpm_event.conf:/usr/local/apache2/mpm_event.conf:ro
      - ./wp_ports.conf:/usr/local/apache2/ports.conf:ro
      - /etc/lego/certificates/hollandnumerics.org.uk.crt:/etc/ssl/certs/hollandnumerics.org.uk.crt:ro
      - /etc/lego/certificates/hollandnumerics.org.uk.key:/etc/ssl/certs/hollandnumerics.org.uk.key:ro
      - ./app-resources.sh:/usr/local/apache2/bin/app-resources.sh:ro
      - ./before-start.sh:/usr/local/apache2/bin/before-start.sh:ro
      - ./start.sh:/usr/local/apache2/bin/start.sh:ro

wp-php-fpm Dockerfile:

# switch to the FPM variant
FROM wordpress:6.9.4-php8.4-fpm

docker-entrypoint.sh:

#!/usr/bin/env bash
set -Eeuo pipefail

	if [ ! -e index.php ] && [ ! -e wp-includes/version.php ]; then
		# if the directory exists and WordPress doesn't appear to be installed AND the permissions of it are root:root, let's chown it (likely a Docker-created directory)
		if [ "$uid" = '0' ] && [ "$(stat -c '%u:%g' .)" = '0:0' ]; then
			chown "$user:$group" .
		fi

		echo >&2 "WordPress not found in $PWD - copying now..."
		if [ -n "$(find -mindepth 1 -maxdepth 1 -not -name wp-content)" ]; then
			echo >&2 "WARNING: $PWD is not empty! (copying anyhow)"
		fi
		sourceTarArgs=(
			--create
			--file -
			--directory /usr/src/wordpress
			--owner "$user" --group "$group"
		)
		targetTarArgs=(
			--extract
			--file -
		)
		if [ "$uid" != '0' ]; then
			# avoid "tar: .: Cannot utime: Operation not permitted" and "tar: .: Cannot change mode to rwxr-xr-x: Operation not permitted"
			targetTarArgs+=( --no-overwrite-dir )
		fi
		# loop over "pluggable" content in the source, and if it already exists in the destination, skip it
		# https://github.com/docker-library/wordpress/issues/506 ("wp-content" persisted, "akismet" updated, WordPress container restarted/recreated, "akismet" downgraded)
		for contentPath in \
			/usr/src/wordpress/.htaccess \
			/usr/src/wordpress/wp-content/*/*/ \
		; do
			contentPath="${contentPath%/}"
			[ -e "$contentPath" ] || continue
			contentPath="${contentPath#/usr/src/wordpress/}" # "wp-content/plugins/akismet", etc.
			if [ -e "$PWD/$contentPath" ]; then
				echo >&2 "WARNING: '$PWD/$contentPath' exists! (not copying the WordPress version)"
				sourceTarArgs+=( --exclude "./$contentPath" )
			fi
		done
		tar "${sourceTarArgs[@]}" . | tar "${targetTarArgs[@]}"
		echo >&2 "Complete! WordPress has been successfully copied to $PWD"
	fi

	wpEnvs=( "${!WORDPRESS_@}" )
	if [ ! -s wp-config.php ] && [ "${#wpEnvs[@]}" -gt 0 ]; then
		for wpConfigDocker in \
			wp-config-docker.php \
			/usr/src/wordpress/wp-config-docker.php \
		; do
			if [ -s "$wpConfigDocker" ]; then
				echo >&2 "No 'wp-config.php' found in $PWD, but 'WORDPRESS_...' variables supplied; copying '$wpConfigDocker' (${wpEnvs[*]})"
				# using "awk" to replace all instances of "put your unique phrase here" with a properly unique string (for AUTH_KEY and friends to have safe defaults if they aren't specified with environment variables)
				awk '
					/put your unique phrase here/ {
						cmd = "head -c1m /dev/urandom | sha1sum | cut -d\\  -f1"
						cmd | getline str
						close(cmd)
						gsub("put your unique phrase here", str)
					}
					{ print }
				' "$wpConfigDocker" > wp-config.php
				if [ "$uid" = '0' ]; then
					# attempt to ensure that wp-config.php is owned by the run user
					# could be on a filesystem that doesn't allow chown (like some NFS setups)
					chown "$user:$group" wp-config.php || true
				fi
				break
			fi
		done
	fi
fi

# start PHP-FPM in the background
php-fpm -D

exec "$@"

All 3 containers have been created, but only the mariadb-chmod and webserver containers start successfully, and wp-php-fpm repeatedly restarts.

What have I missed?..Phil

PS. How do I look at a log from the wp-php-fpm container that keeps restarting?

I have now resolved the “restarting” of the wp-php-fpm container by copying the docker-entrypoint.sh file from the corresponding Docker Wordpress image.

However, now I am getting the following messages in the wp-php-fpm container log:

{"log":"[31-May-2026 10:26:22] NOTICE: fpm is running, pid 1\n","stream":"stderr","time":"2026-05-31T10:26:22.893922333Z"}
{"log":"[31-May-2026 10:26:22] NOTICE: ready to handle connections\n","stream":"stderr","time":"2026-05-31T10:26:23.167173779Z"}

But the browser connection shows the following 503 message:

Service Unavailable
The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.

The webserver container log included the following messages:

{"log":"[Sun May 31 10:30:37.575767 2026] [proxy:error] [pid 212:tid 224] (111)Connection refused: AH00957: FCGI: attempt to connect to 172.21.0.3:9000 (wordpress:9000) failed\n","stream":"stderr","time":"2026-05-31T10:30:37.575944233Z"}
{"log":"[Sun May 31 10:30:37.575801 2026] [proxy_fcgi:error] [pid 212:tid 224] [client 192.168.1.1:50652] AH01079: failed to make connection to backend: wordpress\n","stream":"stderr","time":"2026-05-31T10:30:37.576016127Z"}
{"log":"192.168.1.1 - - [31/May/2026:10:30:37 +0000] \"GET /wp-admin/ HTTP/1.1\" 503 339\n","stream":"stdout","time":"2026-05-31T10:30:37.576077838Z"}

Are you sure fpm still listens on port 9000?

I think port 9000 is still being used by fpm:

  • wordpress image is based on php:8.4-fpm
  • php:8.4-fpm image includes EXPOSE 9000

However, I had changed the wordpress image from wordpress:6.9.4-php8.4-fpm to wordpress:7.0-php8.4-fpm

Looking in the webserver Dockerfile, are these environment variables set appropriately?

    SRV_REVERSE_PROXY_DOMAIN="" \
    SRV_REVERSE_PROXY_CLIENT_IP_HEADER="X-Forwarded-For" \
    SRV_PHP="true" \
    SRV_PHP_DISABLE_REUSE="true" \
    SRV_PHP_HOST="wordpress" \
    SRV_PHP_PORT="9000" \
    SRV_PROXY_PROTOCOL="false" \
    SRV_PROXY_FORWARD_TO="" \
    SRV_PROXY_FORWARD_FROM="/"

Sorry for the slow responses.

The variables look right. Those are the default values in my image, but you don’t need most of those variables.

Try to get the generated httpd config to confirm if it is correct. If not, you can investigate why.

But the logs showed that http tries to use the correct host. If the port is correct too and PHP FPM is still running, that should work.

You could try running a container for debugging and connect it to the compose network used by the PHP service. Something like this:

network_name=projectname-default
docker run --rm -it --network "$network_name"  nicolaka/netshoot

Then try the following commands:

nslookup wordpress
telnet wordpress 9000
# type something and press enter

If that doesn’t work, try this too:

container_name=projectname-wordpress-1
docker run --rm -it --network "container:$container_name"  nicolaka/netshoot

and

telnet 127.0.0.1 9000
# type something and press enter

I didn’t try these commands so could be wrong, but I hope it still helps. I wonder if the issue is caused by dns resolution or fpm not listening on the right IP address. Maybe only on localhost. So while you are still in the container conected to the container network, not the bridge, you can also try

ss -tulpn | grep 9000

That shows if the wordpress container is not correctly listening on the IP address of the container.

NOTE: Don’t forget to change the variable values to your actual network name or container name.

UPDATE: THe port numnber in the last grep was wrong. IIt should have been 9000. fixed it.

Thank you.

I’m going to have to take a longer look into this…

Let me know if you can get some useful info from the outputs of the recommended commands and still need help. I also fixed the last code block where I used the wrong port tnumber.