Skip to content

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-adapter on PATH:
make build
export PATH="$PWD/target/debug:$PATH"
  • 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:

docker compose exec web pwd
docker compose exec web ls /app

Run

  1. Start the container:
docker compose up -d web
  1. Set a breakpoint in a controller and start the session:
bugsaur gui --project . --break src/Controller/OrderController.php:45
  1. Send a request:
curl 'http://localhost:8080/orders?id=1'

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:

pathMappings = { "/app" = "${root}", "/vendor-src" = "${root}/vendor" }

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:

# project B
[profiles.docker.launch_arguments]
port = 9004
pathMappings = { "/app" = "${root}" }
; project B's xdebug.ini
xdebug.client_port=9004

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.