PHP Xdebug Debugging Setup Guide with VS Code

17 min read 3,346 words
PHP Debugging with Xdebug and VS Code: A Practical Guide featured image

Why var_dump() Is Not a Real Debugging Solution

Every PHP developer starts with var_dump(). You spot something behaving unexpectedly, drop in a var_dump() call, reload the page, read the output, remove the statement, and move on. For simple bugs in straightforward code, this approach works well enough. The moment things get complicated, var_dump() becomes a limitation rather than a tool.

Nested arrays produce walls of output that are difficult to read. Conditional logic bugs run without crashing but return wrong results. Issues that only appear in specific requests, API calls, or cron jobs cannot be triggered by simply reloading a browser page. You end up adding var_dump() calls everywhere, guessing where the problem might be, and hoping the output tells you something useful.

The deeper problem with var_dump() is that it requires modifying code to investigate. Every change carries risk of introducing whitespace issues, syntax errors during removal, or new bugs from incomplete edits. You see whatever you explicitly dump, not the full runtime state of the application.

Xdebug is a PHP extension that provides a proper debugger. It lets you set breakpoints, step through execution line by line, inspect variables at any point, and trace the full call stack. Once you have configured Xdebug with VS Code and experienced pausing execution at the exact line where something goes wrong, watching variable values change as you step through logic, the constraints of var_dump() become immediately obvious.

How Xdebug Communicates With Your IDE

Xdebug works as a client-server pair. The PHP script runs with the Xdebug extension active, which listens on a configured port for connections from a debugging client. VS Code acts as the debugging client, connecting to Xdebug on the configured host and port. When a debugging session starts, Xdebug pauses PHP execution and sends the current state to VS Code over this connection.

The communication happens through DBGp, a debugging protocol designed specifically for this purpose. Xdebug listens on client_host:client_port (default: localhost:9003) and waits for a debugging client to connect. Once connected, Xdebug takes control of execution and reports the call stack, variables, and breakpoint status to the client.

Xdebug has several modes that control what it does. Coverage mode instruments code to measure which lines execute, used by PHPUnit for coverage reports. Develop mode provides enhanced error messages and variable output without stopping execution. Debug mode pauses execution and communicates with your IDE. Profiler mode writes data that tools like KCacheGrind can visualise for performance analysis.

You set the mode in your php.ini file. For development, use mode=debug. For production, Xdebug should never be enabled. The performance impact is significant, and exposing the debugging interface creates a real security risk.

Installing Xdebug on Ubuntu with PHP-FPM

On Ubuntu with PHP-FPM, install Xdebug using apt.

sudo apt update
sudo apt install php-xdebug
sudo systemctl restart php8.1-fpm

The package installs Xdebug and enables it automatically in the PHP-FPM configuration directory. On Ubuntu, this is typically /etc/php/8.1/fpm/conf.d/20-xdebug.ini (the version number varies by PHP version). The module loads but the mode may not be configured correctly by default.

cat /etc/php/8.1/fpm/conf.d/20-xdebug.ini
# Usually contains: zend_extension=xdebug.so without mode settings

Add the configuration to set the correct mode and connection parameters. For debugging with VS Code, you need mode=debug, client_host set to the IP address where VS Code is running (127.0.0.1 if on the same machine), and client_port set to 9003.

zend_extension=xdebug.so
xdebug.mode=debug
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
xdebug.start_with_request=trigger
xdebug.idekey=VSCODE

The start_with_request=trigger setting means debugging only activates when a specific trigger is present in the request. This prevents every page load from launching a debugging session, which would become unmanageable very quickly. The trigger is the XDEBUG_SESSION cookie or query parameter.

If VS Code and PHP-FPM run on the same machine, client_host=127.0.0.1 works correctly. If PHP-FPM runs inside a Docker container and VS Code runs on your host, client_host must be the IP address of the host on the Docker bridge network, not localhost. Docker containers cannot reach localhost on the host because localhost refers to the container itself.

# Find the host IP from inside the container
hostname -I | awk '{print $1}'
# Use that IP as xdebug.client_host in the container's php.ini

After changing the configuration, restart PHP-FPM and verify Xdebug loads correctly.

sudo systemctl restart php8.1-fpm
php -m | grep -i xdebug

Installing Xdebug on macOS with Homebrew PHP

Homebrew PHP installations include Xdebug through the Homebrew php tap. If you are using Homebrew's PHP, install Xdebug with brew.

brew install php
brew install xdebug

Homebrew does not configure Xdebug automatically. You need to add the configuration to your php.ini. Find the php.ini location first.

php -i | grep "Loaded Configuration File"
# Output: /opt/homebrew/etc/php/8.3/php.ini

Add the Xdebug configuration to that file.

xdebug.mode=debug
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
xdebug.start_with_request=trigger

Restart PHP to load the new configuration.

brew services restart php
php -m | grep xdebug

Installing Xdebug on Windows with XAMPP

XAMPP ships with Xdebug included but disabled by default. Find the php.ini location through the XAMPP control panel (PHP Settings, Edit php.ini). Look for the Xdebug section and add or uncomment the configuration.

zend_extension = C:\xampp\php\ext\php_xdebug.dll
xdebug.mode=debug
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
xdebug.start_with_request=trigger

If XAMPP does not include the Xdebug DLL, download the correct version from xdebug.org. The download page asks for your PHP version. Run php -v to find your version, download the matching DLL, place it in C:\xampp\php\ext\, and reference it in the zend_extension path.

Restart Apache from the XAMPP control panel. Verify the installation with phpinfo() or php -m from the XAMPP shell.

Configuring VS Code to Listen for Xdebug Connections

Install the PHP Debug extension by Felix Becker in VS Code. This extension implements the DBGp protocol that Xdebug uses to communicate with your IDE. Search for "PHP Debug" in the Extensions panel and click Install.

After installation, create a launch.json file in the .vscode directory of your project. Open the Debug panel (Ctrl+Shift+D on Windows and Linux, Cmd+Shift+D on macOS), click "create a launch.json file", and select PHP. VS Code generates the basic configuration.

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Listen for Xdebug",
      "type": "php",
      "request": "launch",
      "port": 9003,
      "pathMappings": {
        "/var/www/html": "${workspaceFolder}"
      }
    }
  ]
}

The pathMappings entry is critical when your web server document root is on a different path than your VS Code workspace. If your project is at /home/user/project on your local machine but the web server serves from /var/www/html, pathMappings maps the server path to your local path. Without this mapping, VS Code cannot find source files to display when a breakpoint is hit.

If PHP runs inside a Docker container, the pathMappings should point to the container's filesystem path. If your container maps /home/user/project to /var/www/html inside the container, the entry should be "/var/www/html": "/home/user/project". Getting the paths right between your IDE and container prevents common debugging frustrations.

Click the green play icon next to "Listen for Xdebug" to start listening. VS Code shows "Listening for Xdebug" in the debug toolbar. When a PHP request triggers a debugging session, VS Code activates and shows the paused execution with full access to variables, the call stack, and debugging controls.

Starting a Debugging Session

With start_with_request=trigger, the debugging session does not start automatically on every request. You need to pass the XDEBUG_SESSION trigger in the request. There are three practical ways to do this.

The first method is adding ?XDEBUG_SESSION_START=VSCODE as a query parameter to your URL. For example, http://localhost/index.php?XDEBUG_SESSION_START=VSCODE. The XDEBUG_SESSION_START parameter with any value triggers the debugging session. The idekey tells Xdebug which debugging profile to use.

The second method is setting a cookie with the same name in your browser. The Xdebug browser extension (available for Chrome and Firefox) adds this cookie when you click its icon to enable debugging for a site. Once the cookie is set, all requests to that domain trigger a debugging session without the query parameter.

The third method is using the Xdebug helper icon in your browser toolbar. Click it to enable debugging for the current site. Click it again to disable. This is more convenient than adding query parameters for every request when you are actively debugging a particular feature.

If VS Code is not listening when a debug session triggers, Xdebug waits for xdebug.connect_timeout_ms milliseconds (default: 200ms) then times out and continues execution. Make sure VS Code is listening before triggering the session.

Setting and Using Breakpoints Effectively

Click in the margin next to any line number to set a breakpoint. A red circle indicates an active breakpoint. Breakpoints pause execution before the line runs, letting you inspect the application state at that moment.

Right-click on a breakpoint to set conditions. The debugger only pauses when the condition is true. This is useful for debugging loops where you want to pause on a specific iteration or when a particular data state exists.

Conditional breakpoints accept PHP expressions. Set a condition of $user->id === 42 to pause only when that user is processing. Set $total > 1000 to pause only when the total exceeds a threshold. Conditional breakpoints are one of the most useful features that var_dump() cannot replicate.

Logpoints pause execution but output a message to the debug console instead of stopping. Use them to log when a code path executes without interrupting the application. Set a logpoint with the message "User {$user->name} accessed resource {$id}" and it outputs every time the line is hit. This is useful for tracing execution flow without modifying code.

Function breakpoints pause execution when a specific function is called. Set a function breakpoint on "my_function" and execution pauses whenever that function is invoked, regardless of where the call originates.

Stepping Through Code With the Debug Controls

The VS Code debug toolbar provides five primary controls. Continue (F5) runs execution until the next breakpoint or script completion. Step Over (F10) executes the current line and pauses at the next line in the same function. If the current line calls another function, Step Over runs the entire function and pauses after it returns.

Step Into (F11) enters the called function and pauses at its first line. Step Out (Shift+F11) runs the rest of the current function and pauses after it returns. Restart (Shift+F5) ends the session and starts a new one. Stop (Shift+F5) ends the session and disconnects from Xdebug.

The common workflow is to set a breakpoint at the entry point of the area you suspect contains the bug, trigger a debug session, step through with Step Over, and watch variable values change. Step Into a function when you reach a call that might be causing the problem. Step Out when you have verified a function works correctly.

The Call Stack panel shows the chain of function calls that led to the current execution point. Click any frame to view the variables and context at that point. The Variables panel shows local variables and their current values. Expand arrays and objects to inspect their contents in detail.

Tracking Variables With Watch Expressions

The Watch panel monitors specific variables or expressions throughout a debugging session. Add a variable by clicking the + icon and typing the name. Add an expression like $user->orders->total. The Watch panel evaluates the expression at each breakpoint.

Watch expressions persist across breakpoints, so you can track a value as you step through many lines. This is useful for understanding how a value changes across a loop or how an object accumulates state across multiple function calls.

Expressions in the Watch panel support PHP syntax. Watch $result > 0 to see whether a condition is true at each step. When debugging complex problems, watch expressions reduce the time needed to identify the root cause.

Debugging PHP Command-Line Scripts

Xdebug works for CLI scripts as well as web requests. For CLI scripts, the XDEBUG_SESSION environment variable triggers the debugging session.

export XDEBUG_SESSION=VSCODE
php my-script.php

Or use the -d flag to pass Xdebug configuration directly without modifying your php.ini.

php -d xdebug.mode=debug -d xdebug.client_host=127.0.0.1 -d xdebug.start_with_request=trigger my-script.php

This approach is useful for debugging cron jobs, artisan commands, and scripts that run from the command line. CLI debugging is particularly valuable when building automated workflows or background processing tasks. If you are running multiple containers for your PHP projects, the same Docker Compose multi-container setup concepts apply to ensuring your development environment mirrors production.

Resolving Common Xdebug Configuration Issues

The most common issue is a connection timeout. If Xdebug cannot connect to the debugging client within the timeout period, it abandons the connection and execution continues. This happens when client_host is wrong, the port is blocked by a firewall, or the debugging client is not listening when the request is made.

Verify that client_host in php.ini matches the IP address of the machine running VS Code. On macOS or Linux with VS Code on the same machine, 127.0.0.1 is correct. In Docker, use the host's IP on the Docker bridge network, not localhost.

Another common problem is multiple PHP installations. On Ubuntu with Apache and PHP-FPM, apt install php-xdebug may install Xdebug for the CLI version but not for FPM. If you enable Xdebug in the CLI php.ini but not for FPM, web debugging does not work even though CLI debugging does.

# Check CLI php.ini location
php --ini
# Check FPM php.ini location
php-fpm8.1 -i | grep "Loaded Configuration File"
# Edit the correct file for each SAPI

A third common issue is that Xdebug 3 changed configuration parameter names. If you are following older tutorials that mention xdebug.remote_host and xdebug.remote_port, those parameters were renamed. Use client_host and client_port instead.

Debugging PHP-FPM Requests in Docker Environments

When PHP runs under PHP-FPM, the connection works slightly differently. The web server spawns a PHP-FPM worker process to handle the request. The Xdebug extension in that worker tries to connect back to your debugging client. The client must be listening before the request is made.

If VS Code is on the host and PHP-FPM is in a Docker container, the container's PHP process connects to the host's IP, not to localhost. Set client_host in the container's php.ini to the host's gateway IP. This is the most common reason debugging fails for Docker-based PHP setups.

# From inside the container, find the host gateway
ip route | grep default | awk '{print $3}'
# Use that IP as xdebug.client_host in container php.ini

PHP-FPM also has a security.limit_extensions setting that can interfere with Xdebug. If Xdebug is not loading for .php files, check that this setting is not restricting PHP to specific file extensions.

When Xdebug Is the Right Tool

Use var_dump() for quick one-off checks in known small contexts. When you want to verify a variable's type or a single value, adding var_dump() and reloading is faster than starting a debugging session. This approach is appropriate for simple checks when you know exactly where to look.

Use Xdebug for everything else. Bugs that require understanding execution flow, bugs that involve complex state across multiple variables, bugs that only appear under specific conditions, and logical errors where code runs without crashing but produces wrong results are all solved properly with the debugger. The time spent setting up Xdebug pays back immediately on the first complex bug it helps you solve.

Complementing your debugging setup with proper error logging gives you visibility into both local development issues and production problems. Understanding production error logging and monitoring helps you diagnose issues that are difficult to reproduce in a local debugging session.

For projects that involve version-specific features, knowing the PHP 7.4 features worth knowing helps you write code that takes advantage of language improvements while maintaining compatibility with your target PHP version.

Building a Practical PHP Debugging Workflow

Setting up Xdebug with VS Code is a worthwhile investment for any PHP developer. The ability to pause execution, inspect variables, and step through code changes how you approach debugging entirely. Rather than guessing what is happening based on scattered output, you see the actual state of your application at every step.

Once Xdebug is configured and working, take time to explore the less obvious features. Conditional breakpoints save hours when debugging loops or repeated operations. Logpoints let you trace execution without modifying code. The Watch panel helps you track complex object state across function calls. These features become natural once you understand them, and they make debugging significantly faster than var_dump() ever could.

If you need help reviewing your current PHP debugging setup or resolving a specific issue, you can get in touch with details of the problem and what you are trying to achieve.

Frequently Asked Questions

Does Xdebug slow down PHP performance?
Yes, Xdebug significantly impacts performance when active. This is why it should never be enabled in production environments. The start_with_request=trigger setting helps by only activating debugging when explicitly requested. In development, the performance impact is acceptable. In production, even develop mode can slow response times and expose internal application details that should remain private.
Can I debug PHP running inside Docker with VS Code on Windows?
Yes, this works but requires correct configuration. The key point is that a Docker container cannot reach localhost on your Windows host. Set xdebug.client_host to the host's IP address on the Docker network, not 127.0.0.1. Use ip route from inside the container to find the correct gateway IP. The pathMappings in launch.json should map the container's filesystem path to your local workspace path.
Why does Xdebug connect but not stop at my breakpoints?
This usually happens when pathMappings in launch.json does not correctly map the web server's document root to your local project. If PHP sees files at /var/www/html but your VS Code workspace is at /projects/myapp, breakpoints do not align with the running code. Review your pathMappings entry and verify it matches your actual setup.
Can I use Xdebug with the PHP built-in development server?
Yes, Xdebug works with php -S (the built-in development server) on the same machine. The configuration is the same as for Apache or Nginx. Start VS Code listening for Xdebug, then trigger a debugging session with ?XDEBUG_SESSION_START=VSCODE on your URL. The built-in server is useful for quick testing but is not suitable for production.
How do I debug AJAX requests or API calls?
For AJAX requests made by JavaScript, the Xdebug browser extension is the easiest approach. Enable debugging for the page before making the AJAX request, and the cookie causes the PHP backend to trigger a debug session when the request arrives. For API calls made with tools like Postman, add ?XDEBUG_SESSION_START=VSCODE to the request URL or set the Cookie header.
Is Xdebug compatible with PHP 8.x?
Yes, Xdebug 3.x supports PHP 8.0, 8.1, 8.2, and 8.3. Make sure you are using a recent version of Xdebug that matches your PHP version. Older versions may not support newer PHP releases. Use php -v to check your PHP version and ensure compatibility.
What is the difference between Xdebug modes?
Coverage mode instruments code to measure which lines are executed, producing data for code coverage reports. Develop mode provides enhanced error messages and variable output without stopping execution. Debug mode is what you use for step-through debugging with breakpoints and variable inspection. Profiler mode writes performance data that visualisation tools can analyse. You typically use debug mode for development and profiling mode occasionally for performance tuning.