Debug PHP in Docker¶
The most common real-world PHP setup, and the one with the most moving parts: the code runs inside a container, the debugger runs on the host, and the paths do not match.
Prerequisites¶
php-dbgp-adapteronPATH:
- Xdebug installed in the container image
- your project mounted into the container
The container side¶
docker-compose.yml:
services:
web:
build: .
ports:
- "8080:80"
volumes:
- .:/app # host . -> container /app
extra_hosts:
- "host.docker.internal:host-gateway" # needed on Linux
Xdebug's ini, inside the image:
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
host.docker.internal is how a container reaches the host. On Docker Desktop it
exists already; on Linux you add the extra_hosts line above.
The config¶
version = 1
default = "docker"
[profiles.docker]
adapter = "php"
program = "."
[profiles.docker.launch_arguments]
sessionMode = "server"
port = 9003
pathMappings = { "/app" = "${root}" }
pathMappings is what makes this work. Xdebug reports
/app/src/Controller.php; the mapping turns that into
<root>/src/Controller.php, which your editor can open — and turns your
breakpoints back into container paths on the way out.
The left side must match the container path in volumes: exactly. Check rather
than guess:
Run¶
- Start the container:
- Set a breakpoint in a controller and start the session:
- Send a request:
What should happen¶
The request pauses inside the container, the debugger stops at line 45, and the
source shown is your local file — not a path under /app.
Several mounts¶
A project with vendored code mounted separately needs a mapping per mount:
PHPUnit inside the container¶
[profiles.docker.launch_arguments]
sessionMode = "server"
port = 9003
pathMappings = { "/app" = "${root}" }
testCommand = ["docker", "compose", "exec", "-T", "-e", "XDEBUG_TRIGGER=1", "php", "php", "vendor/bin/phpunit"]
Then :DebugTest with the cursor inside a test method runs PHPUnit in the
container with --filter Class::method. The command runs after the adapter is
already listening, so the connection back always finds it.
Two projects at once¶
The DBGp port is per machine. Give each project its own:
When it does not work¶
| Symptom | Cause | Fix |
|---|---|---|
| Nothing ever connects | the container cannot reach the host | check client_host; add extra_hosts on Linux |
| Nothing stops | the request went out before the session started | send another |
| Stops, but no source | pathMappings missing or wrong |
compare with volumes: and docker compose exec web pwd |
| Breakpoints never bind | same cause — host paths never match container paths | as above |
| The wrong file opens | the prefix maps to the wrong directory | check both sides of the mapping |
| Port already in use | another project holds it | different port per project |
Deeper diagnosis: Troubleshooting → Source not found.