How we build and operate Keboola data platform
Ondřej Hlaváček 4 min read

Xdebug for a CLI App in Docker (and PHPStorm)

The majority of my coding are simple CLI applications. We have a nice boilerplate that helps bootstrapping the repository with all the…

Xdebug for a CLI App in Docker (and PHPStorm)

The majority of my coding are simple CLI applications. We have a nice boilerplate that helps bootstrapping the repository with all the required tools (from integration with Travis CI to static code analysis), but I was missing one piece that comes in handy when I need to debug — a debugger. Having Xdebug in your app will give you 2 major benefits:

  • live code debugger (no more var_dump)
  • code coverage generated when running PHPUnit tests

I went through a lot of articles touching on the subject, many of them were outdated, focused on web applications or very confusing. They simply didn't lead to a working debugger in my setup:

  • OS X 10.13.1
  • Docker for Mac 17.09.0-ce
  • PHPStorm 2017.2.4

The application is based on a very simple Dockerfile.

FROM php:7.1-cli
WORKDIR /code
RUN apt-get update && apt-get install -y \
        git \
        unzip \
   --no-install-recommends && rm -r /var/lib/apt/lists/*
RUN curl -sS https://getcomposer.org/installer | php \
  && mv /code/composer.phar /usr/local/bin/composer
COPY . /code/
RUN composer install
CMD php /code/run.php

This code builds the image with the application, runs the tests on Travis CI, and then (if tests pass) deploys it to AWS ECR. Building Xdebug into this image would not be wise, because you don't want to have an image with Xdebug run your production app. So I have another Docker image and a separate Dockerfile just for Xdebug.

FROM php:7.1-cli
WORKDIR /code
RUN apt-get update && apt-get install -y \
        git \
        unzip \
   --no-install-recommends && rm -r /var/lib/apt/lists/*
RUN curl -sS https://getcomposer.org/installer | php \
  && mv /code/composer.phar /usr/local/bin/composer
RUN pecl install xdebug \
  && docker-php-ext-enable xdebug
COPY ./docker/xdebug/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini
COPY . /code/
RUN composer install

It is very similar to the original application image. I could have used the application image as the source (using FROM), but we tend to keep the image structure as flat as possible, so a plain copy&paste in this case. Let's see what commands were added to the original application image.

First, you need to install and enable Xdebug in the image.

RUN pecl install xdebug \
  && docker-php-ext-enable xdebug

And then copy the xdebug.ini setting file.

COPY ./docker/xdebug/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini

The ./docker/xdebug/xdebug.ini is a local relative path, and the file is stored in the repository. The the content of the file took me a while to figure out and is a key to running Xdebug in a CLI app properly.

xdebug.remote_enable=1
xdebug.remote_autostart=1
xdebug.remote_connect_back=0
xdebug.profiler_enable=0

Now you can build the image using

docker build --file Dockerfile-xdebug --tag myapp-xdebug .

The image is built and ready to run. You can now run PHPUnit with the code coverage option. It may need a bit of tweaking of the phpunit.xml.dist file, but the PHPUnit CLI will tell you what to do.

docker run --rm myapp-xdebug /code/vendor/bin/phpunit --coverage-clover build/logs/clover.xml

However, Xdebug needs environment or ini file variables to setup the connection to your host machine for live debugging. You can bake the variables into the ./docker/xdebug/debug.ini file, but I'd recommend against it. Variables like remote_host, remote_port or serverName should not be stored in the repository and are runtime specific.

docker run --rm \
--env "XDEBUG_CONFIG=remote_host=docker.for.mac.localhost remote_port=9000" \
--env "PHP_IDE_CONFIG=serverName=myapp" \
myapp-xdebug php /code/run.php

XDEBUG_CONFIG env variable sets the remote_host and remote_port Xdebug properties. Newer versions of Docker for Mac (17.06 and newer) can conveniently use docker.for.mac.localhost to connect from the Docker container to the host. Other versions (older or other OS) will need to get the IP. remote_port needs to match your IDE settings.

PHP_IDE_CONFIG is used to pair the Xdebug instance and your project code with the IDE.

Now to your IDE. I use PHPStorm, but a different IDE will be set up in a very similar way.

In Preferences > Languages & Frameworks > PHP, add a new CLI Interpreter. Choose the Docker option, and PHPStorm will automatically find the Xdebug image for you.

Then open Preferences > Languages & Frameworks > PHP > Debug, and set the Xdebug port to the same value as remote_port.

Next, you need to set up mappings so that the IDE knows what files Xdebug is talking about. Set up a new server in Preferences > Languages & Frameworks > PHP > Servers. The server name MUST match the serverName value in the docker run command. A host is required, and the localhost value fills in the blank even if it's not really relevant. Don't forget to set the mapping (my project folder is copied to /code in the Docker container when the image is built so the mapping goes from the project root to /code).

Then enable listening for Xdebug in Run > Start Listening for PHP Debug Connections.

With all this setup you can run the application and Xdebug should stop at the first break point in your code.

docker run --rm \
--env "XDEBUG_CONFIG=remote_host=docker.for.mac.localhost remote_port=9000" \
--env "PHP_IDE_CONFIG=serverName=myapp" \
myapp-xdebug php /code/run.php

There you go! I'll leave the tweaking to you, just wanted to show a simple working example. If you want to see it in a real world example, we have a demo repository that we keep up-to-date with the latest tools. I (mis)use Docker Compose to simplify the running commands as you can see in the README file.

If you liked this article please share it.

Comments ()

Read next

MySQL + SSL + Doctrine

MySQL + SSL + Doctrine

Enabling and enforcing SSL connection on MySQL is easy: Just generate the certificates and configure the server to require secure…
Ondřej Popelka 8 min read