Debug your PHP applications running on docker with vscode

Introduction

Setting up debugging tools in a docker environment is rarely as easy as it should. Here is a short tutorial to get it done in PHP with the Xdebug extension. If you are in a hurry you can quickly get all files used in this tutorial from this github repository.

Setting up the php application running on docker

A prerequisite to this tutorial is to have a PHP application running on docker. If it’s already set up feel free to skip this section. For the sake of simplicity I will place all files directly in the project root directory. Obviously I would advise against this and you should rather tidy things up in a folder containing all docker related files.

This recipe will contain:

  • A docker-compose.yml file for our web server running php along with nginx:
version: '3.7'

services:

  php_app:
    build:
      context: .
      dockerfile: Dockerfile-php-app
    volumes:
      - ./:/var/www/html

  nginx_app:
    image: nginx:1.18.0-alpine
    ports:
      - 8080:80
    volumes:
      - ./:/var/www/html
      - ./app.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - php_app

  • The corresponding Dockerfile

I named it Dockerfile-php-app to make it explicit it’s for the PHP image and not the nginx one.

FROM php:7.4-fpm-alpine

# php configuration
COPY php.ini /usr/local/etc/php/

  • Along with a php.ini file

Its content does not matter at the moment but the file is required as we will need to append the Xdebug configuration later on.

[PHP]

;;;;;;;;;;;;;;;;;;;
; About php.ini   ;
;;;;;;;;;;;;;;;;;;;
; PHP's initialization file, generally called php.ini, is responsible for
; configuring many of the aspects of PHP's behavior.

error_reporting = E_ALL;
log_errors = On;
display_errors = On;
error_log = /dev/stderr;

  • Then the nginx configuration to render our php

The file name is app.conf as referenced in the docker-compose file.

server {
    root /var/www/html;

    access_log /dev/stdout;
    error_log /dev/stderr;

    charset utf-8;

    location / {
        # try to serve file directly, fallback to index.php
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index.php(/|$) {
        fastcgi_pass php_app:9000;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}
  • Finally a dummy index.php file with a few instructions will do:
<p>Let's do some math.</p>

<ul>
    <?php
        for ($i = 0; $i < 10; $i++) {
            echo sprintf("<li>%s * 5 = %s</li>", $i, $i*5);
        }
    ?>
</ul>

Run docker-compose up -d and browse http://localhost:8080/. You should be good to go.

Setting up Xdebug with vscode

The first thing we need to do is to update our docker image to include Xdebug. Let’s add these 3 lines to Dockerfile-php-app:

...

# Xdebug configuration
RUN apk add --no-cache $PHPIZE_DEPS # dependencies needed by pecl
RUN pecl install xdebug && docker-php-ext-enable xdebug

Then let’s touch our php.ini file by appending the xdebug configuration:

;xdebug configuration
xdebug.remote_enable = 1
xdebug.remote_port = 9099
xdebug.remote_autostart = 1
xdebug.remote_connect_back = 0
xdebug.remote_host = 192.168.0.30

Run docker-compose build to get the latest version of your image.

Set the remote host to whatever your internal network IP is. On my ubuntu laptop, running hostname -I | awk '{print $1}' on the command line prints it.

Run docker-compose up -d a second time.

Last but not least, you will have to update your vscode configuration. First install the PHP Debug extension by Felix Becker. Then in vscode select the debug tab, then click Add Configuration.. and select the PHP environment. This will open the launch.json file. Erase its content with the following:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for XDebug",
            "type": "php",
            "request": "launch",
            "port": 9099,
            "pathMappings": {
                "/var/www/html/": "${workspaceRoot}"
            }
        }
    ]
}

Add a breakpoint in index.php and hit Listen for XDebug. Browse http://localhost:8080/. Vscode should pop and the code will be at your breakpoint! Make sure vscode is opened for the directory containing all these files but not a parent directory. Else the relative path will be wrong and vscode won’t budge.

Known issues

According to the Xdebug documentation, Xdebug will still not start a debugging session automatically when a script is run. I did not myself experience any issue. A good way to test is to launch the debugger, then run the following:

$ docker-compose exec php_app php index.php

If nothing happens, update the php_app service in the docker-compose.yml file by adding an environment variable:

services:

  php:
    ...
    environment:
      XDEBUG_CONFIG: idekey=session_name

Run these commands once again to check if everything is fine:

$ docker-compose up -d
$ docker-compose exec php_app php index.php

Multiple PHP containers

What if your application is composed of multiple PHP backends? In this case you will need to have them running in other docker containers, and the xdebug port (9099 for now) will conflict. A solution to this problem is to make good use of the Dockerfile ARG instruction.

This is what you get with 2 different php docker images and 2 docker-compose service accordingly:

  • Here is the docker-compose.yml file
version: '3.7'

services:

  php_app_1:
    build:
      context: .
      dockerfile: Dockerfile-php-app
      args:
        XDEBUG_PORT: 9042
    volumes:
      - ./:/var/www/html

  php_app_2:
    build:
      context: .
      dockerfile: Dockerfile-php-app
      args:
        XDEBUG_PORT: 9043
    volumes:
      - ./:/var/www/html

With the following instructions in Dockerfile-php-app, running docker-compose build will result in different docker images with different Xdebug ports.

...

ARG XDEBUG_PORT
RUN sed -i "s/xdebug.remote_port = 9099/xdebug.remote_port = $XDEBUG_PORT/g" /usr/local/etc/php/php.ini

Eventually, for each of your codebase, change the value of the port in your vscode debugger configuration:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for XDebug",
            "type": "php",
            "request": "launch",
            "port": 9043,
            "pathMappings": {
                "/var/www/html/": "${workspaceRoot}"
            }
        }
    ]
}

If you found this tutorial helpful, star this repo as a thank you! ⭐