Connecting Xdebug and PHPUnit in Docker with PhpStorm the Right Way
AI generated
@test
assert
Xdebug 3 · Docker · PhpStorm · PHPUnit · Magento 2 · Coverage
Connecting Xdebug and PHPUnit in Docker with PhpStorm the Right Way
XDEBUG_MODE, path mapping and the remote interpreter

Debugging PHPUnit tests in a Docker environment with PhpStorm is one of the most frustrating parts of PHP development. Configuration usually fails because of XDEBUG_MODE, incorrect path mapping, or a remote interpreter that was never set up correctly. This guide shows the complete path, from installing Xdebug to running coverage reports.

25 min read Xdebug 3 · XDEBUG_MODE · path mapping · remote interpreter · coverage Docker · PhpStorm 2024+ · PHP 8.4 · Mark Shust setup

1. Xdebug 3 vs. Xdebug 2: what changed

Xdebug 3 fundamentally changed the configuration model of Xdebug 2. In Xdebug 2, debugging, profiling and coverage were activated through separate INI directives (xdebug.remote_enable=1, xdebug.coverage_enable=1). In Xdebug 3, a single xdebug.mode field controls all features. The modes are: debug for step debugging, coverage for code coverage, profile for profiling and trace for tracing. Multiple modes are specified as a comma-separated list: debug,coverage.

The most important difference for Docker environments: in Xdebug 3, the mode can be set through the XDEBUG_MODE environment variable without touching php.ini. This makes it possible to keep Xdebug disabled by default (XDEBUG_MODE=off) and activate it only for specific commands. That is considerably better than the Xdebug 2 model, where Xdebug was either globally active or globally inactive, a major reason for the performance problems many developers used to have with Xdebug in Docker.

2. Installing and configuring Xdebug in Docker correctly

In the Mark Shust Docker setup for Magento, Xdebug is enabled and disabled through the wrapper script bin/xdebug enable|disable. Internally, this script toggles the xdebug.ini file in the container's PHP configuration directory. For a manual installation in a different Docker setup, the preferred approach is a separate INI snippet that contains only Xdebug-specific configuration and can be versioned independently of the main php.ini.


; /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
; Xdebug 3 configuration for Docker development environment

[xdebug]
zend_extension=xdebug

; Default mode: off, activated via XDEBUG_MODE environment variable
xdebug.mode=off

; Client host: host.docker.internal resolves to the Docker host on Mac/Windows
; On Linux: use the gateway IP (usually 172.17.0.1) or host-gateway in compose
xdebug.client_host=host.docker.internal
xdebug.client_port=9003

; Disable discovery, PhpStorm will connect automatically on breakpoint
xdebug.discover_client_host=false
xdebug.start_with_request=yes

; IDE Key must match PhpStorm's DBGp Proxy settings
xdebug.idekey=PHPSTORM

; Log to stderr for easy debugging of connection issues
; xdebug.log=/var/log/xdebug.log
; xdebug.log_level=7

There is a special case for Linux hosts: host.docker.internal is not automatically available on Linux (unlike on macOS and Windows). The solution in Docker Compose: extra_hosts: - "host.docker.internal:host-gateway" inside the PHP container's service block. Since Docker 20.10, host-gateway works on all platforms and resolves automatically to the correct host IP. Alternatively, the Docker gateway IP (172.17.0.1 or 172.16.0.1) can be entered directly.

3. XDEBUG_MODE: the most common configuration mistake

The most common mistake with Xdebug 3 in Docker: the xdebug.mode=off setting in the INI file blocks all debugging and coverage, even when Xdebug is loaded. At the same time, many developers forget to set XDEBUG_MODE when they run PHPUnit. The result: no breakpoints, no coverage, no error, just silence. PHPUnit runs, but Xdebug does nothing.

The correct approach: XDEBUG_MODE is set explicitly for every use case. For step debugging: XDEBUG_MODE=debug vendor/bin/phpunit. For coverage: XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html reports/coverage. For both: XDEBUG_MODE=debug,coverage vendor/bin/phpunit. The environment variable overrides the INI value and allows granular control without any INI changes.


# docker-compose.yml, PHP service with Xdebug configuration
services:
  phpfpm:
    image: markoshust/magento-php:8.4-fpm
    volumes:
      - ./src:/var/www/html
    environment:
      # Default: off, enable only when needed
      XDEBUG_MODE: "off"
    extra_hosts:
      - "host.docker.internal:host-gateway"  # Linux compatibility

# .env (project root), override per developer machine
# Uncomment to enable debugging globally (use sparingly)
# XDEBUG_MODE=debug

# bin/xdebug wrapper script, enables/disables per-request
#!/usr/bin/env bash
# Usage: bin/xdebug enable | disable
if [[ "$1" == "enable" ]]; then
  bin/cli bash -c "echo 'xdebug.mode=debug,develop' > /usr/local/etc/php/conf.d/xdebug-mode.ini"
  bin/restart
elif [[ "$1" == "disable" ]]; then
  bin/cli bash -c "echo 'xdebug.mode=off' > /usr/local/etc/php/conf.d/xdebug-mode.ini"
  bin/restart
fi

# Run PHPUnit with coverage, no persistent Xdebug activation needed
bin/cli bash -c "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html reports/coverage"

4. Configuring the PhpStorm remote interpreter

PhpStorm needs to know which PHP interpreter to use for tests. In a Docker environment, that is the PHP interpreter inside the container, not the locally installed PHP interpreter. Configuration happens under Settings → PHP → CLI Interpreter. There, a new interpreter of type "Docker" or "Docker Compose" is added. PhpStorm then connects to the container through the Docker API and runs PHP commands inside the container.

The critical detail: PhpStorm automatically translates file paths between the local filesystem (where the code lives on the host) and the container filesystem (where PHP actually runs the code). This path mapping must be configured correctly so breakpoints in PhpStorm point to the correct running line of code inside the container. An incorrectly configured path mapping is the second most common reason breakpoints in PHPUnit tests are not hit when using the Docker remote interpreter.

5. Path mapping: connecting container paths and local paths

With path mapping, PhpStorm assigns a local directory on the host to a path inside the container. In a typical Magento Docker setup, the Magento code lives locally in ./src and is mounted inside the container under /var/www/html. The path mapping then reads: local path /home/mir/development/mironsoft/src → container path /var/www/html.

If the mapping is missing or wrong, PhpStorm either opens the wrong file at the breakpoint hit, or the breakpoint is never hit at all because PhpStorm cannot resolve the path. The simplest diagnostic test: run a simple PHP script with an echo inside the container and check whether PhpStorm detects the hit. If that works, the problem lies with the path mapping for PHPUnit-specific files.


# PhpStorm Run/Debug Configuration for PHPUnit in Docker
# (Settings -> PHP -> Test Frameworks -> PHPUnit)

# Interpreter: Docker Compose, phpfpm service
# PHPUnit: Use Composer autoloader
# Path to script: /var/www/html/vendor/autoload.php

# Path Mappings in the Interpreter:
# Local path (host):      /home/mir/development/mironsoft/src
# Remote path (container): /var/www/html

# .idea/php.xml, stored by PhpStorm, shows the connection
<component name="PhpInterpreters">
  <interpreters>
    <interpreter id="docker-compose-php84"
                 name="Docker PHP 8.4"
                 home="docker-compose://./compose.dev.yaml:phpfpm:/usr/local/bin/php">
      <path_mappings>
        <path_mapping
          local-root="$PROJECT_DIR$/src"
          remote-root="/var/www/html" />
      </path_mappings>
    </interpreter>
  </interpreters>
</component>

# Verify Xdebug is available in the correct mode:
# bin/cli php -r "var_dump(xdebug_info());"
# Expected: mode = coverage (or debug, depending on XDEBUG_MODE)

6. Running PHPUnit tests directly from PhpStorm

Once the remote interpreter and path mapping are configured correctly, PHPUnit tests can be started directly from the PhpStorm IDE, either by clicking the green arrow next to the test class or through the Run menu. Internally, PhpStorm generates a command that runs PHPUnit inside the container and displays the output in the IDE. The benefit: test names, error messages and stack traces are linked directly in PhpStorm and can be clicked to jump to the corresponding line of code.

For the test run configuration, the phpunit.xml template is decisive. PhpStorm reads the file automatically from the project directory when it is set as the default configuration file. The bootstrap directive must point to the container path (/var/www/html/vendor/autoload.php), not the local path. PhpStorm translates these paths automatically at runtime through path mapping, but the configuration file itself is read by the PHP process inside the container.

7. Viewing coverage reports in PhpStorm

PhpStorm can visualize coverage reports directly in the editor: lines are marked green (covered) or red (not covered), and a coverage percentage per class appears in the sidebar. This feature requires Xdebug to run in coverage mode (XDEBUG_MODE=coverage) and PHPUnit to output a PHP serialization of the coverage data using the --coverage-php option.

The simplest method: in the PHPUnit run configuration in PhpStorm, check the "Collect coverage" box. PhpStorm then automatically sets the necessary PHP flags and loads the coverage data into the editor after the test run. Coverage data is cached in the PhpStorm project index and remains visible across test runs until refreshed by a new coverage run.

Problem Likely cause Solution
Breakpoints are never hit Path mapping wrong or XDEBUG_MODE=off Check path mapping, set XDEBUG_MODE=debug
Coverage file is empty XDEBUG_MODE=coverage missing Set XDEBUG_MODE=coverage as an env variable
PHPUnit cannot find vendor/autoload.php Bootstrap path is a host path instead of a container path Set phpunit.xml bootstrap to /var/www/html/...
host.docker.internal unreachable Linux without extra_hosts configuration extra_hosts: host.docker.internal:host-gateway
PhpStorm "Waiting for connection" Firewall blocks port 9003 Set up a firewall rule for port 9003

8. Diagnosis: when debugging does not work

Systematic diagnosis matters more than trial and error when it comes to Xdebug problems. The first diagnostic step: check whether Xdebug is loaded at all: bin/cli php -m | grep xdebug. If Xdebug does not appear, the extension is not loaded. If Xdebug appears but nothing happens: bin/cli php -r "var_dump(xdebug_info());" shows the current mode and all configuration values.

The second step: enable Xdebug logging. The directives xdebug.log=/tmp/xdebug.log and xdebug.log_level=7 write detailed connection information to a file. The log shows whether Xdebug is trying to connect to PhpStorm and whether the connection fails. Common errors: wrong client IP, wrong port, or PhpStorm not listening on the configured port. The PhpStorm debugger port (9003 by default) must be enabled in PhpStorm's settings and not blocked by a firewall.

9. PCOV vs. Xdebug for coverage compared

For code coverage there is a powerful alternative to Xdebug: PCOV. PCOV was built exclusively for coverage and is considerably faster than Xdebug in coverage mode, typically by a factor of 3 to 5. PCOV does not enable step debugging and has no profiling mode. It is a specialized PHP extension that only collects code coverage data.

The recommendation for production CI pipelines: PCOV instead of Xdebug for coverage jobs. In local development with PhpStorm, Xdebug is still necessary because PhpStorm uses the Xdebug protocol for coverage visualization in the editor. A pragmatic solution: Xdebug for local development (debugging plus coverage in PhpStorm), PCOV for CI coverage jobs (faster, no debugging overhead). PHPUnit itself supports both engines and automatically picks the available one.

10. Summary

Connecting Xdebug 3 in Docker with PhpStorm correctly requires configuring four components properly: the Xdebug INI in the container, the XDEBUG_MODE environment variable, the PhpStorm remote interpreter, and path mapping. Every one of these components has to be correct. A mistake in any single place blocks the entire debugging setup without a clear error message appearing.

The key takeaways: XDEBUG_MODE=off in the INI is the sensible default, Xdebug is only activated for specific commands. XDEBUG_MODE=coverage is mandatory for coverage reports. Path mapping must correctly map the host path to the container path. For Linux hosts, extra_hosts: host.docker.internal:host-gateway is required in Docker Compose. PCOV is considerably faster than Xdebug for CI coverage jobs.

Xdebug and PHPUnit in Docker, the essentials at a glance

XDEBUG_MODE

Default: off in the INI. Activate via environment variable: XDEBUG_MODE=debug for step debugging, XDEBUG_MODE=coverage for coverage. Both: debug,coverage.

Linux host.docker.internal

On Linux, Docker Compose needs extra_hosts: - "host.docker.internal:host-gateway" in the PHP service. Without it, the debugger port is unreachable.

Path mapping in PhpStorm

Map the host path to the container path. Typical: /home/user/project/src/var/www/html. Without a correct mapping, breakpoints are never hit.

PCOV for CI

PCOV is 3 to 5 times faster than Xdebug for coverage. No debugging overhead. Recommended for CI coverage jobs. Keep using Xdebug locally with PhpStorm.

11. FAQ: Xdebug and PHPUnit in Docker with PhpStorm

1Why are breakpoints in PHPUnit tests never hit?
XDEBUG_MODE not set to 'debug', wrong path mapping, or PhpStorm not listening on port 9003. Diagnosis: bin/cli php -r "var_dump(xdebug_info());"
2Why is the coverage file empty?
Xdebug 3 requires XDEBUG_MODE=coverage. Without this environment variable, Xdebug collects no coverage data, even when it is loaded.
3Configuring host.docker.internal on Linux?
In docker-compose.yml for the PHP service: extra_hosts: - "host.docker.internal:host-gateway". Available on all platforms since Docker 20.10.
4PCOV vs. Xdebug for coverage?
PCOV is 3 to 5 times faster, no debugging overhead. Xdebug for local development with PhpStorm, PCOV for CI coverage jobs.
5Configuring the PhpStorm remote interpreter for Docker Compose?
Settings -> PHP -> CLI Interpreters -> + -> Docker Compose. Specify the compose file and the 'phpfpm' service. PhpStorm reads the PHP version and extensions automatically.
6PHPUnit cannot find vendor/autoload.php?
The bootstrap directive in phpunit.xml must point to the container path: bootstrap="/var/www/html/vendor/autoload.php". PHPUnit runs inside the container and does not see host paths.
7Enabling Xdebug logging for diagnosis?
xdebug.log=/tmp/xdebug.log and xdebug.log_level=7 in the INI. The log shows every connection attempt with the error causes.
8Disabling Xdebug by default?
Recommended approach: xdebug.mode=off in the INI. For specific commands: XDEBUG_MODE=debug vendor/bin/phpunit. No permanent performance hit.
9First diagnostic steps when debugging does not respond at all?
1. Is Xdebug loaded? 2. Is the mode correct? 3. Is PhpStorm listening on port 9003? 4. Is a firewall blocking the port? 5. Is host.docker.internal reachable? Check all five points systematically.
10Running PHPUnit with coverage in the terminal?
In the Mark Shust setup: bin/cli bash -c 'XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html reports/coverage/html'. Create the directory beforehand with mkdir -p.