Profiling and Debugging a PHP app with Xdebug and Docker

Profiling and Debugging a PHP app with Xdebug and Docker

I have started using an IDE again (PHPStorm) so that I could debug some applications and do some basic app profiling. I want to use Xdebug to profile my PHP apps. I am using Docker Compose on Windows 10. I have made this very complicated for myself but here we go.

The directory structure of my app looks like:

/build/docker/php/Dockerfile
/build/docker/php/php.ini
/build/docker/nginx/Dockerfile
/build/docker/nginx/default.conf
/web (contains my php app)
docker-compose.yml

First thing is to get Xdebug setup in the PHP container. I am using a custom Dockerfile for my PHP container where I install a ton of additional modules and packages, install wp-cli, and copy a custom php.ini to the container.

Here is the entire Dockerfile for the PHP container:

FROM php:7.0-fpm

# Install some required tools
RUN apt-get update && apt-get install -y sudo less

# Install PHP Extensions
RUN apt-get update && apt-get install -y \
bzip2 \
libbz2-dev \
libc-client2007e-dev \
libjpeg-dev \
libkrb5-dev \
libldap2-dev \
libmagickwand-dev \
libmcrypt-dev \
libpng12-dev \
libpq-dev \
libxml2-dev \
mysql-client \
imagemagick \
xfonts-base \
xfonts-75dpi \
&& pecl install imagick \
&& pecl install oauth-2.0.2 \
&& pecl install redis-3.0.0 \
&& pecl install xdebug \
&& docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \
&& docker-php-ext-configure imap --with-imap-ssl --with-kerberos \
&& docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ \
&& docker-php-ext-enable imagick \
&& docker-php-ext-enable oauth \
&& docker-php-ext-enable redis \
&& docker-php-ext-enable xdebug \
&& docker-php-ext-install \
bcmath \
bz2 \
calendar \
gd \
imap \
ldap \
mcrypt \
mbstring \
mysqli \
opcache \
pdo \
pdo_mysql \
soap \
zip \
&& apt-get -y clean \
&& apt-get -y autoclean \
&& apt-get -y autoremove \
&& rm -rf /var/lib/apt/lists/* && rm -rf && rm -rf /var/lib/cache/* && rm -rf /var/lib/log/* && rm -rf /tmp/*

# Custom PHP Conf
COPY ./php.ini /usr/local/etc/php/conf.d/custom.ini

# WP-CLI
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
&& mv wp-cli.phar /usr/local/bin \
&& chmod +x /usr/local/bin/wp-cli.phar \
&& ln -s /usr/local/bin/wp-cli.phar /usr/local/bin/wp

# Xdebug
RUN mkdir /tmp/xdebug
RUN chmod 777 /tmp/xdebug

# Run this container as "webuser"
RUN groupadd -r webuser && useradd -r -g webuser webuser
RUN usermod -aG www-data webuser
USER webuser

custom php.ini under ./build/docker/php/:

file_uploads = On
memory_limit = 512M
upload_max_filesize = 256M
post_max_size = 256M
max_execution_time = 600
display_errors = On
error_reporting = E_ALL

[XDebug]
xdebug.profiler_output_dir = "/tmp/xdebug/"
xdebug.profiler_output_name = "cachegrind.out.%t-%s"
xdebug.profiler_append = 1
xdebug.profiler_enable_trigger = 1
xdebug.trace_output_dir = "/tmp/xdebug/"
xdebug.remote_enable = on
xdebug.remote_autostart = true
xdebug.remote_handler = dbgp
xdebug.remote_mode = req
xdebug.remote_port = 9999
xdebug.remote_log = /tmp/xdebug/xdebug_remote.log
xdebug.idekey = MYIDE
xdebug.remote_connect_back = 1

Some important things here. I am creating a directory to store Xdebug output (/tmp/xdebug) which will be used by another container to parse and display the output. In the custom php.ini we tell Xdebug to store its output to this directory. We also configure Xdebug to enable remote debugging so that we can debug from our IDE. If you do not want to debug EVERY request you should disable remote_autostart. If you do this you need to pass in a specific GET/POST parameter to trigger the debugger (typically XDEBUG_PROFILE). Make note of the remote_port and idekey values. We need these when we configure our IDE.

In your IDE you would configure Xdebug to listen on port 9999 for connections and to use the IDE Session Key MYIDE to ensure you are only debugging requests that use that session key (really only necessary for complicated setups with multiple apps on the same server).

There are two environment variables that I set on the PHP container that are required to make this all work.

docker-compose.yml

php:
build: ./build/docker/php/
expose:
- 9000
links:
- mysql
volumes:
- .:/var/www/html
- /tmp/xdebug
environment:
XDEBUG_CONFIG: "remote_host=192.168.99.1"
PHP_IDE_CONFIG: "serverName=XDEBUG"

XDEBUG_CONFIG is required to tell Xdebug where the client is running. To be honest, I am not sure if this is actually required or is only required because of PHPStorm. I am using Docker Toolbox and am using the IP from the VirtualBox VM where the Docker env is running. It would be great to not have to have this param as it would be more portable.

The variable PHP_IDE_CONFIG though is required for PHPStorm, and it tells my IDE which server configuration to use.

Neither of these may be required if you are using native docker and a different IDE.  /shrug

The first part of this is done. We can now debug an app from our IDE. The second thing I wanted to do was run a profiler and inspect the results. Xdebug will output cachegrind files. We just need a way to inspect them. There are some desktop apps you can use, like KCacheGrind, QCacheGrind, WinCacheGrind, etc. Your IDE may even be able to parse them (PHPStorm is currently no able to for some reason). Or you can use a web based system. I opted for a web based system using WebGrind. There is, conveniently, a docker container for this.

I configured the php container to expose /tmp/xdebug as a shared volume, which is where Xdebug is configured too output cachegrind files. Then I configured the webgrind container to mount that volume. Also I pass an environment variable to tell WebGrind where to find the cachegrind files:

docker-compose.yml

webgrind:
    image: devgeniem/webgrind
    ports:
        - 8081:80
    volumes_from:
        - php
    environment:
        XDEBUG_OUTPUT_DIR: "/tmp/xdebug"

With that we can go to http://192.168.99.100:8081 and start digging into the app profiles.

--

Complete docker-compose.yml

mysql:
    image: mysql:latest
    volumes_from:
        - mysql_data
    environment:
        MYSQL_ROOT_PASSWORD: secret
        MYSQL_DATABASE: project
        MYSQL_USER: project
        MYSQL_PASSWORD: project
    expose:
        - 3306

mysql_data:
    image: tianon/true
    volumes:
        - /var/lib/mysql

nginx:
    build: ./build/docker/nginx/
    ports:
        - 80:80
    links:
        - php
    volumes:
        - .:/var/www/html

php:
    build: ./build/docker/php/
    expose:
        - 9000
    links:
        - mysql
    volumes:
        - .:/var/www/html
        - /tmp/xdebug
    environment:
        XDEBUG_CONFIG: "remote_host=192.168.99.1"
        PHP_IDE_CONFIG: "serverName=XDEBUG"

phpmyadmin:
    image: phpmyadmin/phpmyadmin
    ports:
        - 8080:80
    links:
        - mysql
    environment:
        PMA_HOST: mysql

webgrind:
    image: devgeniem/webgrind
    ports:
        - 8081:80
    volumes_from:
        - php
    environment:
        XDEBUG_OUTPUT_DIR: "/tmp/xdebug"