Xdebug is the one PHP extension almost everybody installs, and for years the command was pecl install xdebug. The Xdebug install page now says that PECL and its website are deprecated, and it lists pie install xdebug/xdebug as the recommended route. The new command is easy. The part that bites people later is leaving Xdebug switched on where it should not be, because it makes PHP a lot slower. This guide covers both: installing it with PIE, and making sure it only runs where you want it.
Before you start
- PIE installed. If you do not have it yet, the PIE installation guide covers it in a few minutes.
-
Build tools and PHP development files on Linux and macOS, because PIE compiles Xdebug from source. Run
pie check-build-toolsto see what is missing. On Debian or Ubuntu you also need the dev package for your PHP version, for examplephp8.3-dev, which providesphpize. - Windows downloads a prebuilt binary instead, so no compiler is needed.
Install Xdebug
pie install xdebug/xdebug
PIE downloads the source, runs phpize, configure and make, copies xdebug.so into your extension directory and tries to enable it. On an Ubuntu 24.04 test machine with PHP 8.3 the last lines looked like this:
Install complete: /usr/lib/php/20230831/xdebug.so
Enabled extension xdebug in the INI file /etc/php/8.3/mods-available/xdebug.ini
Successfully asserted that extension xdebug is loaded in runtime.
Extension xdebug/xdebug:3.5.3 is enabled and loaded in /usr/bin/php8.3
You do not edit php.ini yourself. PIE wrote the zend_extension=xdebug line, in a file with priority 90 so it loads after OPcache, which is the order the Xdebug docs ask for. The exact file depends on your system: here it was mods-available/xdebug.ini linked into conf.d as 90-xdebug.ini, and on other setups PIE adds the line to php.ini. The Xdebug docs also say to ignore the old PECL prompt that suggests adding extension=xdebug.so, since that causes problems.
Check that it loaded
php -v
# ... with Xdebug v3.5.3, Copyright (c) 2002-2026, by Derick Rethans
pie show
# Loaded PIE extensions:
# xdebug:3.5.3 (from xdebug/xdebug:3.5.3)
If you run PHP-FPM or Apache, restart them afterwards. The command line and FPM often use different ini directories, so check the one that matters with php --ini (CLI) or a page that calls xdebug_info().
Pick a mode, because the default is not what you want
Xdebug does nothing useful until you tell it which features to turn on, and each one has a cost. The setting is xdebug.mode. On Xdebug 3.5.3 the default I measured was develop, which is not what most people expect.
| Mode | What it gives you |
|---|---|
debug |
Step debugging from your IDE (breakpoints, stepping) |
develop |
Better var_dump() output and stack traces |
coverage |
Code coverage for PHPUnit |
profile |
Profiler output for tools like KCachegrind |
trace |
Function-call trace files |
gcstats |
Garbage collector statistics |
off |
Loaded, but every feature disabled |
You can combine them with commas (debug,develop). You can also override the ini file for a single command with the XDEBUG_MODE environment variable, which is handy in CI:
XDEBUG_MODE=coverage vendor/bin/phpunit
I checked that XDEBUG_MODE=coverage reports coverage as the only active mode through xdebug_info("mode"), so the variable really does win over the ini value.
Set up step debugging
Put your own settings in a separate file so a later pie upgrade never touches them. For example /etc/php/8.3/cli/conf.d/99-xdebug-local.ini:
xdebug.mode = debug
xdebug.start_with_request = trigger
xdebug.client_host = 127.0.0.1
xdebug.client_port = 9003
Port 9003 is Xdebug 3's default (Xdebug 2 used 9000), so make sure your IDE listens on 9003. start_with_request = trigger means Xdebug only connects when you ask it to, instead of on every single request. For a CLI script, the trigger is an environment variable:
XDEBUG_TRIGGER=1 php artisan my:command
To prove the connection works without an IDE, I started a plain socket listener on port 9003 and ran a script both ways. With trigger and no XDEBUG_TRIGGER, nothing connected. With XDEBUG_TRIGGER=1, Xdebug connected and sent its DBGp init packet. For web requests the Xdebug docs describe a browser trigger (a cookie or query parameter), which I did not test here. In Docker, client_host usually has to point at the host machine rather than 127.0.0.1, for example host.docker.internal. That one is a common setup, not something I ran.
The cost of leaving it on
This is the part that matters for production. I ran a call-heavy script (a recursive Fibonacci, about seven million function calls) on the same PHP 8.3 build:
| Setup | Time |
|---|---|
| Xdebug not loaded | about 0.09 s |
Loaded, xdebug.mode=off
|
about 0.09 s |
xdebug.mode=debug |
about 1.2 s |
xdebug.mode=develop |
about 1.6 s |
That script is close to a worst case, since it does nothing except call functions. A real application spends time in databases and I/O, so the slowdown will be smaller, and you should measure yours. But the pattern is clear: loaded with mode=off costs almost nothing, while a live mode costs a lot. That is why a forgotten Xdebug on a server is such a common cause of "why is production so slow".
Keep Xdebug out of production
Use more than one safeguard, in this order of preference.
1. Do not install it where it is not needed
Production images and servers should simply not have it. The simplest rule is to install it only in your development image or on developer machines.
2. Declare it as a dev dependency
PIE can read the extensions your project needs from Composer. If Xdebug is in require-dev:
{
"require": { "php": "^8.3" },
"require-dev": { "ext-xdebug": "*" }
}
then this installs project extensions, skipping the dev-only ones:
pie install-extensions-for-project --no-dev
In PIE 1.5.2 the help text for --no-dev says it excludes extensions declared in the root package's require-dev. I tested it with a project whose only extension requirement was ext-xdebug in require-dev: with --no-dev PIE reported "Finished checking extensions" and installed nothing, and without the flag it built and installed Xdebug. Two practical notes: the command reads composer.lock and failed with "No lockfile found" in a project that had none, and if PIE cannot work out which package provides an extension you can name it with --select=xdebug=xdebug/xdebug.
3. If it must be present, turn it off
xdebug.mode = off
As the table above shows, that brings the cost back to roughly nothing. It is a fallback, though. Not loading the extension at all is still safer, because Xdebug's debugger and develop output are things you never want reachable on a public server.
Upgrade and remove it
pie upgrade # upgrades all PIE-installed extensions
pie uninstall xdebug/xdebug # disables and removes it
On my test machine uninstall deleted xdebug.so and commented out the load line (; zend_extension=xdebug ; removed by PIE). The ini file and the conf.d link stayed, which is harmless. php -v no longer showed Xdebug.
Common problems
-
phpize: not foundor build failures. Install the build tools and your PHP version's dev package, then runpie check-build-tools. PIE can also install missing tools for you with--auto-install-build-tools. -
Installed, but
php -vdoes not show Xdebug. You probably installed into a different PHP than the one you run. Point PIE at the right one with--with-php-config=/usr/bin/php-config8.3, then restart PHP-FPM or Apache. -
Xdebug twice. If you used
pecl installbefore, search your ini files for a secondzend_extensionline (grep -r xdebug /etc/php) and remove the old one. -
A new PHP version. The
composer.jsonin the Xdebug 3.5.3 tag declaresphp: >=8.0,<8.6. So that release is not meant for PHP 8.6 builds, and I did not test what PIE does if you try. Check before you upgrade PHP; I covered what is changing in PHP 8.6.
Frequently asked questions
Is pecl install xdebug still working?
The Xdebug docs still list it as the legacy method, and they say PECL is deprecated. The PECL deprecation is a recommendation change, so existing setups keep working, but new setups should use PIE.
Does Xdebug slow PHP down if mode is off?
In my test, no measurable difference. Any other mode adds overhead, and the more function calls your code makes, the more you notice it.
Which port does Xdebug 3 use?
9003 by default. Xdebug 2 used 9000, so an old IDE configuration is a frequent reason a fresh install "does not connect".
References
Originally published on DEV Talk.
Top comments (0)