I have used many development environments in the past, from the local LEMP stack to Virtual Machines and Vagrant etc. There were always issues among the machines. Sometimes due to minor differences in the programming language or the database version, other times due to misconfiguration or a mistake during the set-up.
Docker solves many of those problems but in a slightly different way. For the sake of simplicity I will keep my examples as minimal as possible, thus many aspects of a real development environment are missing. For a complete setup with FrankenPHP, Caddy and Symfony, see my php-docker (opens in a new tab) repo, which ships with Xdebug ready to use.
First of all, the code! Nothing fancy, just a simple “Hello World” script:
<?php
function sayHello($name) {
$textHello = "Hello";
return $textHello . " " . $name;
}
echo sayHello("world");Dockerfile
To build my PHP Docker image, I am using the following Dockerfile:
FROM php:8.4-cli-alpine
COPY --from=ghcr.io/mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions xdebug \
&& echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
&& echo "xdebug.start_with_request=trigger" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
WORKDIR /var/www/html
CMD ["php", "-S", "0.0.0.0:80"]I use the php:8.4-cli-alpine as my base image. I prefer Alpine Linux since it generates smaller Docker images but any other Linux distribution should work.
It is the CLI image because the example runs PHP’s built-in web server. If you run PHP-FPM behind Nginx or Caddy, use the matching -fpm image instead.
Then I install Xdebug with install-php-extensions (opens in a new tab), which takes care of the build dependencies and picks the Xdebug version that fits the PHP version.
If you prefer pecl, apk add --no-cache --virtual .build-deps $PHPIZE_DEPS linux-headers && pecl install xdebug && docker-php-ext-enable xdebug && apk del .build-deps does the same job.
Two settings go into the Xdebug ini file:
xdebug.client_host=host.docker.internaltells Xdebug where PhpStorm is: on the host machine, outside the container.xdebug.start_with_request=triggerstarts a debug session only when you ask for one, so normal requests stay fast.
There is no EXPOSE for Xdebug. Xdebug connects out of the container to PhpStorm on port 9003, so nothing has to be opened on the container for it.
Lastly I start the PHP local development server php -S 0.0.0.0:80.
Compose
Even though my example is very simple and I could just use a single Dockerfile, I use Docker Compose because the result is more readable and a lot easier to configure than a Docker command.
The file is compose.yaml, and it no longer needs a version: line (Docker Compose ignores it and warns about it):
services:
php:
build: .
ports:
- "8080:80"
volumes:
- .:/var/www/html
environment:
XDEBUG_MODE: debug # off to disable
PHP_IDE_CONFIG: serverName=docker
extra_hosts:
- host.docker.internal:host-gatewayXDEBUG_MODEturns the step debugger on. Set it tooffwhen you don’t need it, without rebuilding the image.PHP_IDE_CONFIGtells PhpStorm which server configuration (and path mappings) to use. It matters most for CLI scripts, which have no host name.extra_hostsmakeshost.docker.internalwork on Linux. Docker Desktop on Mac and Windows defines it already, Docker Engine on Linux does not.
Start it with docker compose up -d --build and the script answers on http://localhost:8080.
PhpStorm
First, create a server configuration under Settings → PHP → Servers:
- Name:
docker, the same as inPHP_IDE_CONFIG - Host:
localhost, port8080, debuggerXdebug - Check Use path mappings and map the project folder to
/var/www/html
The path mapping is the link between the location of the app in the host machine and the container. Without it PhpStorm cannot match the files Xdebug reports to the files you have open.
Then click Start Listening for PHP Debug Connections in the toolbar (or in the Run menu).
Next, I place some breakpoints in index.php, and finally I visit the endpoint on my localhost on port 8080 with a trigger, so that Xdebug starts a session:
- add
?XDEBUG_TRIGGER=1(or?XDEBUG_SESSION=PHPSTORM) to the URL:http://localhost:8080/?XDEBUG_TRIGGER=1 - or use an Xdebug browser extension (opens in a new tab), which sets the
XDEBUG_SESSIONcookie for you
Xdebug should stop the execution of the script on the first breakpoint and now you should be able to debug.
CLI scripts work the same way, with the trigger as an environment variable:
docker compose exec -e XDEBUG_TRIGGER=1 php php index.phpTroubleshooting
When the breakpoints are ignored, I go through these checks in order.
Is Xdebug loaded? The version should be in the output:
docker compose exec php php -v
# with Xdebug v3.x.xAre the settings right? This shows the active mode, client_host, start_with_request and the port:
docker compose exec php php --ri xdebugNothing connects at all.
- Did you send a trigger? With
start_with_request=trigger, a plain request runs without the debugger. - Is PhpStorm listening? The Start Listening toggle has to be on.
- Is
XDEBUG_MODEset todebug? Withoff, a trigger does nothing. - On Linux, is
extra_hostsin place, and does your firewall allow port 9003 from the Docker networks? - Coming from Xdebug 2? The default port changed from 9000 to 9003, and the
xdebug.remote_*settings are nowxdebug.client_*(see the upgrade guide (opens in a new tab)).
It connects, but the breakpoints are skipped. The path mapping is wrong, or PHP_IDE_CONFIG does not match the server name in PhpStorm.
Still stuck? Turn on the Xdebug log for one request. It says exactly where Xdebug tried to connect:
docker compose exec -e XDEBUG_TRIGGER=1 -e XDEBUG_CONFIG="log=/tmp/xdebug.log log_level=3" php sh -c 'php index.php; cat /tmp/xdebug.log'
# ERR: Could not connect to debugging client. Tried: host.docker.internal:9003Depending on your setup and configuration, your Xdebug and PhpStorm settings might need to be adjusted. Useful information can be found on the PhpStorm (opens in a new tab) and Xdebug (opens in a new tab) documentation.