Path Mapping, IDE Integration, and Performance Modes
Xdebug only works reliably in Docker containers once you understand the three most common pitfalls: incorrect host detection, missing path mapping, and Xdebug overhead during normal operation. Solve these three issues properly once, and debugging in Docker becomes just as comfortable as debugging locally.
Table of Contents
- 1. Why Xdebug Often Doesn't Work in Docker
- 2. How Xdebug and Docker Communicate
- 3. Installing Xdebug in the Dockerfile
- 4. Configuring xdebug.ini Correctly
- 5. Host IP Detection on macOS, Linux, and WSL2
- 6. Path Mapping: The Most Common Mistake Explained
- 7. PhpStorm Integration Step by Step
- 8. VS Code with the PHP Debug Extension
- 9. Xdebug Modes Compared
- 10. Summary
- 11. FAQ
1. Why Xdebug Often Doesn't Work in Docker
Xdebug in Docker is conceptually different from local debugging: the PHP process runs inside the container, while the IDE runs on the host. Xdebug initiates the connection itself, actively connecting to the IDE rather than the other way around. This means Xdebug needs to know the IP address of the IDE host, and that host must be reachable on the configured port (9003 by default). On a local system without network separation this is trivial, but in Docker with its own virtual networks it becomes a real challenge.
The three most common mistakes when setting up Xdebug in Docker are: first, a misconfigured or unresolvable host IP; second, missing or incorrect path mapping between the paths inside the container and the local paths; and third, leaving Xdebug enabled during normal operation, which significantly slows down every request through instrumentation. Once all three points are configured correctly, you get reliable debugging without surprises, whether you use PhpStorm, VS Code, or the CLI debugger.
2. How Xdebug and Docker Communicate
Xdebug 3 uses the DAP protocol (Debug Adapter Protocol) over a TCP connection. When PHP executes a file and Xdebug is active, the extension opens a TCP connection to the configured client address (formerly called the remote host) on the configured port. The IDE waits on the other end of that connection, either permanently or after you click the "Listen for Xdebug Connections" button. Once the connection is established, Xdebug sends debug information and receives control commands such as continue, step over, and set breakpoint.
In an Xdebug and Docker setup, the container therefore needs to be able to resolve the IDE's address. Docker containers live in their own virtual network and reach the host through the Docker network's gateway IP or through a special hostname. The host firewall must accept the Xdebug port from the container's IP. On Linux you may need to add an iptables rule; on macOS and Docker Desktop the gateway IP is automatically available on the host.
# Dockerfile.dev: install Xdebug for development builds only
FROM php:8.4-fpm
# Install Xdebug via pecl (do NOT use apt-get xdebug packages, they are outdated)
RUN pecl install xdebug-3.4.0 \
&& docker-php-ext-enable xdebug
# Create a separate xdebug.ini, do NOT mix it with php.ini
# The file must be loaded by PHP's additional ini scan directory
COPY docker/php/xdebug.ini /usr/local/etc/php/conf.d/99-xdebug.ini
# Verify installation
RUN php -v | grep -i xdebug
# Note: the production Dockerfile should NOT install Xdebug
# Use multi-stage builds to keep production images lean
3. Installing Xdebug in the Dockerfile
The cleanest way to install Xdebug in Docker is via pecl install xdebug inside the Dockerfile. The official PHP Docker image ships with docker-php-ext-enable, which automatically places the extension configuration in the right spot. Installation should happen in a dedicated development Dockerfile, not in the production image. Multi-stage builds let you reuse the same base image and add Xdebug only in the development stage, while the production stage stays lean.
A common mistake is mixing Xdebug configuration with the general PHP configuration in php.ini. It is better to use a dedicated file, 99-xdebug.ini, that only contains Xdebug directives. The number 99 in the prefix ensures the file loads after all other ini files and can override their settings. This file can be switched between development and debug mode through a bind mount or an environment variable, without ever rebuilding the image.
4. Configuring xdebug.ini Correctly
Xdebug 3 completely overhauled the configuration system compared to Xdebug 2. The most important difference: there are no longer separate remote_enable and remote_connect_back directives. Instead, the xdebug.mode parameter controls which features are active. The debug mode enables step debugging, coverage enables code coverage measurement, and profile enables the profiler. Multiple modes can be combined using commas.
The xdebug.client_host directive replaces the old remote_host and specifies the IP address or hostname where the IDE can be reached. For Xdebug in Docker on macOS, that is the special hostname host.docker.internal, which automatically points to the host IP. On Linux this hostname must be configured explicitly, either in the container's hosts file or through a Compose extra host entry. With xdebug.discover_client_host=true, Xdebug tries to read the client IP from the HTTP header, which works in some proxy setups but is less reliable than an explicit IP.
; docker/php/xdebug.ini: Xdebug 3 configuration for Docker development
; Only enable what is needed, each mode adds overhead
; Modes: debug (step debugger), coverage, profile, trace, off
; Use XDEBUG_MODE env var to override without rebuilding the image
xdebug.mode = ${XDEBUG_MODE:-off}
; IDE host: host.docker.internal resolves on macOS and Docker Desktop for Windows
; On Linux: use the Docker bridge IP (usually 172.17.0.1) or add extra_hosts
xdebug.client_host = ${XDEBUG_CLIENT_HOST:-host.docker.internal}
; Default port for Xdebug 3 (changed from 9000 in Xdebug 2)
xdebug.client_port = 9003
; Trigger debug sessions by environment variable or query param
; Allows debugging specific requests without enabling globally
xdebug.start_with_request = trigger
; Limit connection attempts to prevent hanging on missing IDE
xdebug.connect_timeout_ms = 2000
; Show detailed error information in browser (useful for development)
xdebug.show_error_trace = 1
xdebug.var_display_max_depth = 5
xdebug.var_display_max_data = 1024
5. Host IP Detection on macOS, Linux, and WSL2
Host IP detection is the most common reason Xdebug in Docker fails to connect. On macOS and Docker Desktop for Windows, the hostname host.docker.internal automatically resolves to the host IP, which is the simplest approach and should always be tried first. On Linux this hostname does not exist by default; it must be configured explicitly in the Compose file via extra_hosts or a custom DNS entry.
A more robust alternative for Linux is reading the Docker gateway IP at runtime: docker network inspect bridge --format '{{(index .IPAM.Config 0).Gateway}}' returns the IP of the Docker bridge through which the container reaches the host. This IP can be passed as the XDEBUG_CLIENT_HOST environment variable when the container starts. Combining xdebug.mode = ${XDEBUG_MODE:-off} with xdebug.client_host = ${XDEBUG_CLIENT_HOST:-host.docker.internal} in the ini file lets you configure Xdebug in Docker in a platform-independent way, without ever having to rebuild the image.
6. Path Mapping: The Most Common Mistake Explained
Path mapping is necessary because file paths inside the container differ from the paths on the host. Inside the container the code lives under /var/www/html/src/, while on the host the same code lives under /home/user/projects/myapp/src/. When Xdebug reports a breakpoint hit to the IDE, it reports the container path. The IDE has to translate that container path into the local path in order to open the correct file. If the mapping is wrong, the IDE either opens the wrong file or shows an error message.
In PhpStorm, path mapping is configured under Run → Edit Configurations → PHP Remote Debug → Server. The server must have the same name that is configured in the PHP_IDE_CONFIG=serverName=myserver environment variable, since this value links the Xdebug session to the correct server profile. In VS Code, path mapping happens in launch.json under pathMappings. Incorrect or missing path mapping typically shows up as breakpoints that can be set but are never triggered, or as the IDE opening the wrong file after connecting.
# compose.override.yml: enable Xdebug without modifying the main compose file
# Use: docker compose -f compose.yml -f compose.override.yml up
services:
php:
environment:
# Enable step debugger mode
XDEBUG_MODE: debug
# On Linux: replace with the actual Docker bridge IP
XDEBUG_CLIENT_HOST: host.docker.internal
# Must match PhpStorm server name (Run → Edit Configurations → Server)
PHP_IDE_CONFIG: "serverName=myapp-local"
extra_hosts:
# Linux: add host.docker.internal manually, pointing to the bridge IP
- "host.docker.internal:host-gateway"
# VS Code launch.json (store in .vscode/launch.json)
# {
# "version": "0.2.0",
# "configurations": [{
# "name": "Listen for Xdebug (Docker)",
# "type": "php",
# "request": "launch",
# "port": 9003,
# "pathMappings": {
# "/var/www/html": "${workspaceFolder}/src"
# }
# }]
# }
7. PhpStorm Integration Step by Step
PhpStorm offers the best native integration for Xdebug in Docker. The setup process involves three steps: server configuration, debug configuration, and linking them via PHP_IDE_CONFIG. Under Settings → PHP → Servers you create a new server with the name that will later be used in PHP_IDE_CONFIG. This is also where you configure the path mappings: the local path on the left, the container path on the right. The Use path mappings option must be explicitly enabled.
Next, under Run → Edit Configurations you create a new PHP Remote Debug configuration that points to the server you just created and uses PHPSTORM as the IDE key (or whatever value is configured as xdebug.idekey). Clicking the green phone button activates PhpStorm's listener on port 9003 and starts waiting for incoming Xdebug connections. If everything is configured correctly and Xdebug in the container runs in debug mode, the first request triggers a popup asking whether to accept the incoming connection.
8. VS Code with the PHP Debug Extension
For VS Code, the PHP Debug extension by Xdebug (Felix Becker / xdebug.org) is the standard solution for Xdebug in Docker. Configuration happens entirely in the .vscode/launch.json file. The most important field is pathMappings: a JSON object where the container paths are the keys and the local host paths are the values. The ${workspaceFolder} variable points to the folder VS Code has open and can be used for relative paths.
Unlike PhpStorm, VS Code does not need a server name in PHP_IDE_CONFIG, since the mapping happens entirely through path mapping. The debug listener is started via the Run → Start Debugging menu or the F5 key. A common pitfall in VS Code is the hostname parameter in the launch configuration: by default VS Code only listens on localhost (127.0.0.1). In Docker environments you must set "hostname": "0.0.0.0" so that connections from container IPs are accepted, otherwise Xdebug successfully connects to the host IP, but the VS Code listener rejects the connection.
9. Xdebug Modes Compared
Xdebug 3 supports several modes that enable different features and have different performance implications. Choosing the right mode for a given use case is essential for productive work with Xdebug in Docker.
| Mode | Function | Performance Overhead | Use Case |
|---|---|---|---|
| off | Xdebug disabled | Minimal | Normal operation, no debugging needed |
| debug | Step debugger (breakpoints) | Medium (only when triggered) | Interactive debugging in the IDE |
| coverage | Code coverage measurement | High (always active) | PHPUnit with a coverage report |
| profile | Cachegrind profiler | Very high | Performance analysis with KCachegrind |
| develop | var_dump improvements | Low | More readable var_dump output in the browser |
Using environment variables to set XDEBUG_MODE=off during normal operation and XDEBUG_MODE=debug during active debugging is the recommended pattern for Xdebug in Docker. With xdebug.start_with_request=trigger, Xdebug only starts a debugging session when a specific cookie, query parameter, or HTTP header is set, so Xdebug can stay permanently active in debug mode without instrumenting every request. This makes selective debugging possible without switching the environment variable and restarting the container.
Mironsoft
PHP development environments, Docker setup, and IDE integration
Is Xdebug in Docker still not running reliably?
We set up Xdebug in your Docker containers, configure path mapping for PhpStorm and VS Code, and make sure debugging sessions start reproducibly, without manual intervention.
Xdebug Setup
Set up the Dockerfile, ini configuration, host IP detection, and path mapping
IDE Integration
Configure and test PhpStorm and VS Code for Xdebug in Docker
Team Rollout
Create compose.override.yml and documentation for the whole development team
10. Summary
Xdebug in Docker works reliably when three things are correct: the host address is configured correctly and reachable, path mapping is set up in the IDE, and Xdebug only runs in debug mode when you are actually debugging. The special hostname host.docker.internal solves the host IP problem on macOS and Windows; on Linux, extra_hosts: host.docker.internal:host-gateway in the Compose file achieves the same thing. Path mapping connects container paths to host paths and can be found in PhpStorm under server configurations, and in VS Code inside launch.json.
Environment variables for XDEBUG_MODE make switching between debug and normal operation trivial. A compose.override.yml keeps the debug configuration out of the main compose file and prevents accidentally deploying with Xdebug active. With xdebug.start_with_request=trigger and the "Xdebug Helper" browser plugin, debugging can be triggered selectively per request without changing the container mode. This combination of configuration, IDE setup, and deliberate mode control turns Xdebug in Docker into a reliable tool instead of a source of frustration.
Docker and Xdebug: The Essentials at a Glance
Host IP
host.docker.internal on macOS/Windows. On Linux: configure extra_hosts with host-gateway in the Compose file.
Path Mapping
Configure container path ↔ host path in the IDE. PHP_IDE_CONFIG=serverName must match the server name in PhpStorm.
Mode Control
XDEBUG_MODE=off during normal operation, XDEBUG_MODE=debug while debugging. start_with_request=trigger for selective sessions.
VS Code
Set hostname: 0.0.0.0 in launch.json, otherwise connections from container IPs are rejected even with correct Xdebug configuration.