How to Add XDebug to the Official Docker WordPress Image

It is intended for a Development environment only. Please do not use this in the Production environment. Please use the official Docker WordPress image at Production instead.

We’ll consider below how to separate configuration for the Production and Development environments using multiple docker-compose files.

This article describes how to add XDebug to the official Docker WordPress image. You can see the resulting image on Docker Hub. Also, you can see the project on GitHub.

For WordPress with XDebug you can build the image from source (recommended). Or you can use a ready-made image.

1. Building an Image

To build an image go to the directory where your docker-compose.yml is located and clone the Git repository:

git clone https://github.com/wpdiaries/wordpress-xdebug.git xdebug

This will clone the Git repository wpdiaries/wordpress-xdebug to the subfolder xdebug.

The Dockerfile of the cloned project will look something like this:

FROM wordpress:5.5.1-php7.4-apache

# Install packages under Debian
RUN apt-get update && \
    apt-get -y install git

# Install XDebug from source as described here:
# https://xdebug.org/docs/install
# Available branches of XDebug could be seen here:
# https://github.com/xdebug/xdebug/branches
RUN cd /tmp && \
    git clone git://github.com/xdebug/xdebug.git && \
    cd xdebug && \
    git checkout xdebug_2_9 && \
    phpize && \
    ./configure --enable-xdebug && \
    make && \
    make install && \
    rm -rf /tmp/xdebug

# Copy xdebug.ini to /usr/local/etc/php/conf.d/
COPY files-to-copy/ /

# Since this Dockerfile extends the official Docker image `wordpress`
# and since `wordpress` in turn extends the offcicial Docker image `php`,
# the the helper script docker-php-ext-enable (defined for image `php`)
# works here and we can use it to enable xdebug:
RUN docker-php-ext-enable xdebug

You can replace the first line with e.g.

FROM wordpress

to build on the latest version of the image wordpress. Or you could add the tag you need to build on some particular version of the image.

Now we could use a docker-compose.yml file e.g. like this:

version: '3.8'

services:

  wordpress:
    container_name: wordpress-wpd
    restart: always
    build:
      dockerfile: Dockerfile # this line is actually redundant here - you need it only if you want to use some custom name for your Dockerfile
      context: ./xdebug # a path to a directory containing a Dockerfile, or a url to a git repository

    ports:
      - "80:80"

    environment:
      VIRTUAL_HOST: mydomain.com, www.mydomain.com
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: mydbname
      WORDPRESS_DB_USER: mydbuser
      WORDPRESS_DB_PASSWORD: mydbpassword
      # Set the XDEBUG_CONFIG as described here: https://xdebug.org/docs/remote
      XDEBUG_CONFIG: remote_host=192.168.1.2

    depends_on:
      - db

    volumes:
      - /opt/projects/wpd/www:/var/www/html

    networks:
      - backend-wpd
      - frontend-wpd


  db:
    container_name: mysql-wpd
    image: mysql:8.0.20
    command: --default-authentication-plugin=mysql_native_password
    restart: always

    environment:
      MYSQL_ROOT_PASSWORD: mydbrootpassword
      #MYSQL_RANDOM_ROOT_PASSWORD: '1' # You can use this instead of the option right above if you do not want to be able login to MySQL under root
      MYSQL_DATABASE: mydbname
      MYSQL_USER: mydbuser
      MYSQL_PASSWORD: mydbpassword

    ports:
      -  "3306:3306" # I prefer to keep the ports available for external connections in the Development environment to be able to work with the database
                     # from programs like e.g. HeidiSQL on Windows or DBeaver on Mac.

    volumes:
      - /opt/projects/wpd/mysql:/var/lib/mysql

    networks:
      - backend-wpd


networks:
  frontend-wpd:
  backend-wpd:

Of course you need to set parameters like database name, user, password etc. to your own values here.

Also, you need to change the IP 192.168.1.2 to the IP of the host machine where your IDE (e.g. PhpStorm or NetBeans) is installed:

XDEBUG_CONFIG: remote_host=192.168.1.2

Te variable XDEBUG_CONFIG is the XDebug environment variable. This variable is described in detail in the XDEbug official documentation.

You can see that we have port 3306 open here for external connections. This will allow us to work with the database from programs like e.g. HeidiSQL on Windows or DBeaver on Mac. Of course, we would never do anything like that in the Production environment. But this configuration is intended for the Development environment only.

Now if you run the docker-compose with

docker-compose up -d

, you will get the containers for WordPress + Xdebug and for MySQL up and running.

If you need to rebuild the WordPress image for any reason (e.g. you have changed the version of wordpress in the Dockerfile), you could run the command:

docker-compose up -d --build

This would rebuild the the WordPress with XDebug image and relaunch the container.

2. Using a Ready-made Image

If you do not want to build anything but prefer to use a prebuilt image, some images are available for you at wpdiaries/wordpress-xdebug.

Tags for ready-made images for wpdiaries/wordpress-xdebug on Docker Hub correspond to the corresponding tags of the official Docker WordPress image.

E.g. wpdiaries/wordpress-xdebug:5.5.1-php7.4-apache means that the image has been created based on the image wordpress:5.5.1-php7.4-apache.

To use the image with docker-compose, you do not need to clone any project from the git repository. And you need to replace the following lines in the docker-compose.yml from the previous example:

    build:
      dockerfile: Dockerfile
      context: ./xdebug

with e.g.

image: wpdiaries/wordpress-xdebug:5.5.1-php7.4-apache

In this case the complete docker-compose.yml will look like this:

version: '3.8'

services:

  wordpress:
    container_name: wordpress-wpd
    restart: always
    image: wpdiaries/wordpress-xdebug:5.5.1-php7.4-apache

    ports:
      - "80:80"

    environment:
      VIRTUAL_HOST: mydomain.com, www.mydomain.com
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: mydbname
      WORDPRESS_DB_USER: mydbuser
      WORDPRESS_DB_PASSWORD: mydbpassword
      # Set the XDEBUG_CONFIG as described here: https://xdebug.org/docs/remote
      XDEBUG_CONFIG: remote_host=192.168.1.2

    depends_on:
      - db

    volumes:
      - /opt/projects/wpd/www:/var/www/html

    networks:
      - backend-wpd
      - frontend-wpd


  db:
    container_name: mysql-wpd
    image: mysql:8.0.20
    command: --default-authentication-plugin=mysql_native_password
    restart: always

    environment:
      MYSQL_ROOT_PASSWORD: mydbrootpassword
      #MYSQL_RANDOM_ROOT_PASSWORD: '1' # You can use this instead of the option right above if you do not want to be able login to MySQL under root
      MYSQL_DATABASE: mydbname
      MYSQL_USER: mydbuser
      MYSQL_PASSWORD: mydbpassword

    ports:
      -  "3306:3306" # I prefer to keep the ports available for external connections in the Development environment to be able to work with the database
                     # from programs like e.g. HeidiSQL on Windows or DBeaver on Mac.

    volumes:
      - /opt/projects/wpd/mysql:/var/lib/mysql

    networks:
      - backend-wpd


networks:
  frontend-wpd:
  backend-wpd:

Now if you run the docker-compose with

docker-compose up -d

, you will get the containers for WordPress + Xdebug and for MySQL up and running. No building in this case is necessary.

Here. the same as in the first example, we have ports 80 (for HTTP server) and 3306 (for MySQL server) open for incoming connections.

Of course this also should be used in the Development environment only. And should never be used on Production.

3. Configuring PhpStorm

If you write your PHP code in the PhpStorm, here are a few brief tips on configuring debugger in this IDE.

First, you can install the XDebug browser extension. I usually debug from Chrome so I use the Google Chrome extension. But extensions for other browsers also exist. Please see this PhpStorm documentation page for the list of extensions you can use.

The Chrome browser extension has been created by XDebug developers, not by PhpStorm developers as you can see in the XDebug documentation here.

Now before you start debugging you need to set a breakpoint in PhpStorm (by clicking on the corresponding line on the left of your code in the IDE).

Also, you will need to map the remote file path to your root directory at the server to the file path in your local file system. To do that in PhpStorm yo will need to go to:

Preferences > Languages & Frameworks > PHP > Debug > Servers

and set the file mapping. In my case (the IDE is installed on a Mac and the project is deployed under Ubuntu on an Oracle VirtualBox virtual machine), the WordPress installed in the folder www. And in the Docker container, the root directory for WordPress is /var/www/html. So I had to set mapping from www to /var/www/html so that PhpStorm new which directory at the Docker container corresponds to the root directory of the project. E.g. for wpdiaries.com the settings window would look similar to this:

Path mapping in PhpStorm
Path mapping in PhpStorm

It is enough to map only the root directory of the project here (at least if you do not have symbolic links). The IDE will guess the paths for subdirectories down below the tree. So, in this case, I have mapped only www (the path to the root of the project PhpStorm sees in my local filesystem) to /var/www/html(the path to the root of the project XDebug sees on the web-server).

Now you will need to turn on debugging in the Chrome extension you’ve installed:

XDebug extension for Google Chrome
XDebug extension for Chrome

Now click the Start Listening button Debugging off in PhpStorm (top left corner of the IDE screen). The button will start looking like this Debugging on. Or you could do the same via the top menu:

Run > Start Listening for PHP Debug Connections

If you reload your site page in the browser now, you’ll start debugging.

You can read more about debugging in PhpStorm in the PhpStorm official documentation.

Over the years of PHP programming, I’ve had the experience of using several IDE for debugging PHP code. With PhpStorm I’ve had the best experience. Other IDE I worked with were either more complicated to configure (which is tolerable of course) or gave me problems like hanging and refusing to debug till relaunched. With PhpStorm things have been much better. At least this is my personal experience. Yes, I couldn’t resist the temptation to say a few good words about my favorite IDE. ­čÖé

4. Using Multiple docker-compose Files in Development and Production Environments

Personally I never use 1 docker-compose file with all the definitions at once. This contradicts the DRY (Don’t Repeat Yourself) principle. I think it is much better to keep common for Production and Development configuration in one docker-compose file. And add one separate docker-compose file defining additional things necessary in Development. And one more additional docker-compose defining things for Production.

You can find more information on using multiple compose files for Production and Development environments in this article in the official Docker documentation. Also, please check this article on using compose in Production.

4.1 Development Environment

Let’s consider the Development environment first.

This is an example of docker-compose.yml with common for Development and Production configuration options:

version: '3.8'

services:

  wordpress:
    container_name: wordpress-wpd
    restart: always

    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: mydbname
      WORDPRESS_DB_USER: mydbuser
      WORDPRESS_DB_PASSWORD: mydbpassword

    depends_on:
      - db

    volumes:
      - /opt/projects/wpd/www:/var/www/html

    networks:
      - backend-wpd
      - frontend-wpd

  db:
    container_name: mysql-wpd
    image: mysql:8.0.20
    command: --default-authentication-plugin=mysql_native_password
    restart: always

    environment:
      MYSQL_ROOT_PASSWORD: mydbrootpassword
      #MYSQL_RANDOM_ROOT_PASSWORD: '1' # You can use this instead of the option right above if you do not want to be able login to MySQL under root
      MYSQL_DATABASE: mydbname
      MYSQL_USER: mydbuser
      MYSQL_PASSWORD: mydbpassword


    volumes:
      - /opt/projects/wpd/mysql:/var/lib/mysql

    networks:
      - backend-wpd

networks:
  frontend-wpd:
  backend-wpd:

The separate docker-compose file with specific for Development environment configuration docker-compose.dev.yml will look like this:

version: '3.8'

services:

  wordpress:
    build:
      context: ./xdebug # a path to a directory containing a Dockerfile, or a url to a git repository

    ports:
      - "80:80"

    environment:
      VIRTUAL_HOST: mydomain.com, www.mydomain.com
      # Set the XDEBUG_CONFIG as described here: https://xdebug.org/docs/remote
      XDEBUG_CONFIG: remote_host=192.168.1.2

  db:
    ports:
      -  "3306:3306"

Of course you need to substitute your own data as it’s been explained in the previous examples.

Now you can start the docker containers with the command:

docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

If you made any changes to Dockerfile, you can rebuild the WordPress container and restart with the command:

docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build

If you need to stop the containers, you will need to run the corresponding command listing the same docker-compose files you used when you started the containers:

docker-compose -f docker-compose.yml -f docker-compose.dev.yml down

which will stop and remove the containers.

Similarly, you could run commands like

4.2 Production Environment

Let’s consider using multiple compose files for Production now.

Please note that we consider only the barebone configuration for Production here. I did not take into account security when wrote this example. You need to check Docker documentation and security articles to secure your installation on Production. This code is an example only. So enough with the disclaimer, let’s move to the configuration files…

4.2.1 Adding Nginx as Reverse Proxy

I prefer to use Nginx as the reverse proxy for all sites, running behind it. I use jwilder/nginx-proxy image for it (see jwilder/nginx-proxy on Docker Hub and corresponding project on GitHub).

As in the previous sections, I prefer to have the docker-compose file to be separated into 2: one for Development and one for Production.

Those 2 files I normally place to the directory /opt/projects/common/ on the host machine.

The docker-compose.yml file for the Development environment looks pretty simple:

version: '3.8'

services:
  proxy:
    container_name: nginx-proxy
    image: jwilder/nginx-proxy
    restart: always

    ports:
      - "80:80"
      - "443:443"

    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro

    # If the logging section is not commented,
    # docker-compose logs
    # will show the HTTP server logs.
    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"

    networks:
      - frontend-wpd
      # other networks go here

networks:
  frontend-wpd:
    name: frontend-wpd # or it will be created as common_frontend-wpd
  # other networks go here

This is an example for one project behind the Nginx proxy server. Normally I have several projects running behind the proxy. In this case, I would add a separate network for each of the projects.

This docker-compose.yml is good for starting project in this configuration in the Development environment as:

docker-compose up -d

In the Development environment, everything is very simple. E.g. I do not need SSL in the Development environment. So e.g. Let’s Encrypt certificates are not added here.

At Production I add the following docker-compose.prod.yml containing additional to the file above configuration:

version: '3.8'

services:
  proxy:
    volumes:
      - /opt/projects/common/nginx/certs:/etc/nginx/certs:ro
      - /opt/projects/common/nginx/vhost.d:/etc/nginx/vhost.d
      - /opt/projects/common/nginx/html:/usr/share/nginx/html
      - /opt/projects/common/nginx/conf.d:/etc/nginx/conf.d

  docker-gen:
    image: jwilder/docker-gen
    command: -notify-sighup nginx-proxy -watch -only-exposed /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
    container_name: gen-proxy
    restart: always

    depends_on:
      - proxy

    volumes:
      - /opt/projects/common/nginx/certs:/etc/nginx/certs:ro
      - /opt/projects/common/nginx/vhost.d:/etc/nginx/vhost.d
      - /opt/projects/common/nginx/html:/usr/share/nginx/html
      - /opt/projects/common/nginx/conf.d:/etc/nginx/conf.d
      - /opt/projects/common/nginx/htpasswd:/etc/nginx/htpasswd:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro

    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"

    networks:
      - frontend-wpd
      # other networks go here

  letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    container_name: letsencrypt-proxy
    restart: always

    environment:
      DEFAULT_EMAIL: test@example.com # replace with your e-mail
      NGINX_DOCKER_GEN_CONTAINER: gen-proxy
      NGINX_PROXY_CONTAINER: nginx-proxy

    depends_on:
      - proxy
      - docker-gen

    volumes:
      - /opt/projects/common/nginx/certs:/etc/nginx/certs:rw
      - /opt/projects/common/nginx/vhost.d:/etc/nginx/vhost.d
      - /opt/projects/common/nginx/html:/usr/share/nginx/html
      - /opt/projects/common/nginx/conf.d:/etc/nginx/conf.d
      - /var/run/docker.sock:/var/run/docker.sock:ro

    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"

    networks:
      - frontend-wpd
      # other networks go here

Also this configuration requires the file nginx.tmpl. You can download it like this:

cd /opt/projects/common
curl -o nginx.tmpl https://raw.githubusercontent.com/jwilder/docker-gen/master/templates/nginx.tmpl

as described here.

For adding Let’s Encrypt we used jrcs/letsencrypt-nginx-proxy-companion. For examples of usage please see:

In the example above we use the 3 container setup.

When getting certificates from Let’s Encrypt, take into account the service has certain limitations. So e.g. if you are debugging and for some reason trying to get certificates for the same domains again and again, you could be blocked from getting certificates for 1 hour. The number of certificates per week and the number of renewals per week are also limited.

Now in the Production environment, I start the Docker containers for the reverse proxy as:

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

When I need to stop the containers and remove them I use the command:

docker-compose -f docker-compose.yml -f docker-compose.prod.yml down

Nginx proxy Docker container always needs to be started first. Other Docker containers (for WordPress and other projects running behind the proxy) should be started after the proxy. This is because the Docker container for proxy creates our networks (frontend-wpd in the example above). Other containers join these networks. If other Production containers are started while the networks (they need to join) do not exist yet, you’ll get an error.

4.2.2 Adding Additional docker-compose File for Our WordPress Project

Now in addition to our docker-compose.yml, considered in section Development Environment (and located in the directory /opt/projects/wpd), we need to add the docker-compose file with additional settings for the Production environment. Let’s add the file docker-compose.prod.yml:

version: '3.8'

services:

  wordpress:
    image: wordpress

    expose:
      - "80"

    environment:
      VIRTUAL_HOST: wpdiaries.com, www.wpdiaries.com
      LETSENCRYPT_HOST: wpdiaries.com, www.wpdiaries.com

  db:


networks:
  frontend-wpd:
    external: true  # which also means this network must already exist before the container is run
    name: frontend-wpd  # or it will look for wpd_frontend-wpd
  backend-wpd:  

You see that we are using the official Docker WordPress image here.

Also, we use expose instead of ports so our port 80 will be available to Nginx proxy but not available directly for external incoming connections.

Also notice that we join the network frontend-wpd here.

Now we can start our WordPress project (after we have started the Nginx reverse proxy container):

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

And if we need to stop and remove the containers, we need to run (before stopping Nginx reverse proxy container):

docker-compose -f docker-compose.yml -f docker-compose.prod.yml down

5. What if I Need to Add a PHP Extension to the WordPress Image?

The official Docker WordPress image is very minimal. It does not contain many PHP extensions which could be possibly required by WordPress plugins. The image is kept minimal to keep its size smaller (as explained in the section Adding additional libraries / extensions of the official WordPress image documentation).

But the official Docker WordPress image is based on the official PHP image. And the PHP image contains simple ways of adding PHP extensions. 3 helper scripts exist for it:

  • docker-php-ext-configure
  • docker-php-ext-install
  • docker-php-ext-enable

Let’s consider how to add a PHP extension to our WordPress image.

E.g. imagine we are trying to install a WordPress plugin and getting the following error message: Fatal error: Uncaught Error: Undefined class constant ‘MYSQL_ATTR_USE_BUFFERED_QUERY’. We understand that we need to install the PHP extension pdo_mysql.

First, we need to login to our WordPress container terminal and check if the extension pdo_mysql has been installed already. Our WordPress container name (in the examples above) is wordpress-wpd. So we log in to the container terminal using docker exec like this:

docker exec -it wordpress-wpd bash

and check if the extension has been installed:

php -m | grep pdo
php -i | grep pdo

This shows that pdo_mysql wasn’t installed.

To install pdo_mysql we need to add the following line to our Dockerfile:

RUN docker-php-ext-install pdo_mysql

For the Development environment (see section 1) we would just add this line to the end of the Dockerfile.

As to the Production environment, we could create our Dockerfile like this:

FROM wordpress:5.5.1-php7.4-apache

RUN apt-get update

RUN docker-php-ext-install pdo_mysql

And if the Dockerfile is located e.g. in the directory ./images/prod, our Production environment file docker-compose.prod.yml from section 4.2.2 would look like this:

version: '3.8'

services:

  wordpress:
    dockerfile: Dockerfile # this line is actually redundant here - you need it only if you want to use some custom name for your Dockerfile
    context: ./images/prod # a path to a directory containing a Dockerfile, or a url to a git repository

    expose:
      - "80"

    environment:
      VIRTUAL_HOST: wpdiaries.com, www.wpdiaries.com
      LETSENCRYPT_HOST: wpdiaries.com, www.wpdiaries.com

  db:


networks:
  frontend-wpd:
    external: true  # which also means this network must already exist before the container is run
    name: frontend-wpd  # or it will look for wpd_frontend-wpd
  backend-wpd:  

Now we can build our image and run the docker container (like in section 4.2.2 – after we have started the Nginx reverse proxy container):

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build

And we can stop and remove these containers by running (must be done before stopping the Nginx reverse proxy container):

docker-compose -f docker-compose.yml -f docker-compose.prod.yml down

Conclusion

Thank you for reading this article to the end. I hope you’ve liked it.

A few final notes:

1) You probably know that Docker is used without a hypervisor (such as VirtualBox, VMPlayer, HyperV etc.) on Linux. Docker is container virtualization. It’s supposed to be used without a hypervisor. On Windows 10, Docker seems to run natively but in fact, uses a HyperV virtual machine with Linux behind the scenes. Docker for Mac also uses a hypervisor (based on xhyve) with a Linux virtual machine.

So, if you would like to install Docker for development purposes on a Mac or Windows computer and you would like to use your favorite Linux distribution there, you would need the Oracle VirtualBox (or another hypervisor like VMPlayer). In this case, please check this article on how to install the VirtualBox on Mac and Windows, create a virtual machine, and tune it for better performance.

2) If you have any questions or comments, I would be very glad if you posted them below.

It would be really great to hear from you.

Your feedback will be highly appreciated.

2 thoughts on “How to Add XDebug to the Official Docker WordPress Image”

    1. Hello. Thanks for the good words! I really appreciate them.

      Yes, of course. I’ve pushed the image for WP 5.5.1 / PHP 7.2 to the Docker Hub repository. The image name is

      wpdiaries/wordpress-xdebug:5.5.1-php7.2-apache

      Just for the benefit of any possible future readers of this comment: all the currently available tags of the docker image wpdiaries/wordpress-xdebug could be found here:
      https://hub.docker.com/r/wpdiaries/wordpress-xdebug/tags

Leave a Reply

Your email address will not be published. Required fields are marked *