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.
Table of Contents
- 1. Xdebug 3 vs. Xdebug 2: what changed
- 2. Installing and configuring Xdebug in Docker correctly
- 3. XDEBUG_MODE: the most common configuration mistake
- 4. Configuring the PhpStorm remote interpreter
- 5. Path mapping: connecting container paths and local paths
- 6. Running PHPUnit tests directly from PhpStorm
- 7. Viewing coverage reports in PhpStorm
- 8. Diagnosis: when debugging does not work
- 9. PCOV vs. Xdebug for coverage compared
- 10. Summary
- 11. FAQ
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?
bin/cli php -r "var_dump(xdebug_info());"2Why is the coverage file empty?
XDEBUG_MODE=coverage. Without this environment variable, Xdebug collects no coverage data, even when it is loaded.3Configuring host.docker.internal on Linux?
extra_hosts: - "host.docker.internal:host-gateway". Available on all platforms since Docker 20.10.4PCOV vs. Xdebug for coverage?
5Configuring the PhpStorm remote interpreter for Docker Compose?
6PHPUnit cannot find vendor/autoload.php?
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?
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?
10Running PHPUnit with coverage in the terminal?
bin/cli bash -c 'XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html reports/coverage/html'. Create the directory beforehand with mkdir -p.