Laravel Docker Xdebug



Running specific PHP versions for Laravel can be quite useful, especially when working with legacy applications. I work on a range of different solutions with different versions of PHP and Laravel versions. To save me time reconfiguring my local environment’s PHP version and to better represent the live systems, I have opted for Docker based development environments. Here’s what I am aiming for:

  1. Laravel Docker Debug Vscode
  2. Xdebug Php Docker
  3. Laravel Docker Debug
  4. Docker Php Fpm Xdebug

Install Xdebug plugin for PHP and config Xdebug to play well on Docker Double-check if the PHP version installed in your Docker container already has Xdebug (by using command php -v and see if the output says anything about Xdebug). If you don’t already have Xdebug installed, you need to install it before continuing. Step Two: Configuring Xdebug. Before Docker, Xdebug was relatively straightforward to configure on a platform, in that it was a new set of the php.ini parameters – you’d either just edit the existing php.ini, or load in a custom ini or override.

  • Customisable PHP versions
    • Including libraries like Imagick and XDebug to make dev easier
  • Self contained database instance
  • Supporting queue worker, so I can test queues work locally
  • Email catching, so I can test email notifications locally
  • Redis, for queue management
  • The Laravel Scheduler working

In order to achieve this, I’ve opted to use a docker-compose environment with custom docker PHP file. This defines the PHP version as well as any extra libraries in it that I need for the project. Then the project files (source code of the Laravel application) can be mounted as a volume. By mounting the project’s source code, it’s available for an editor on the host machine, while also being available for the PHP code to execute.

Let’s start by defining the project structure:

This structure tends to keep the Docker configuration and extra files neater, since they’re self-contained in a `.docker` directory. The custom PHP docker file (Dockerfile.app) is contained here, as is a subdirectory for Nginx, the webserver I’ll be using. Only the docker-compose file needs to be in the parent folder.

Lets start with the docker file. You’ll need to find your host user and group ID. On Linux (and presumably Mac) you can find this by running id -u and id -g. Normally they’re both 1000. Replace the ARG entries in the docker file if your IDs are different.

If you’ve not created the directory structure already, do it now:

mkdir -p .docker/nginx

Now create the Docker file, I’m using Nano but you can use whatever editor you want: nano .docker/Dockerfile.app

I’ve left in some commented commands, which can be uncommented and customised if needed. The file comments should also help you make any changes as needed, but the file should work for you as is.

Next, lets create the nginx configuration file nano .docker/nginx/default.conf

The most important part of this file is the fastcgi_pass php:9000; line. This tells nginx in it’s container where to find PHP running in it’s container. You’ll see that tie in the docker compose file.

Create the docker-compose.yml file nano docker-compose.yml

This is quite a big file. Each container is defined inside the service block. Most are provided containers from dockerhub. There’s a few important things to know (which are mostly commented in the file).

The Nginx container has ports exposed. I’ve set these to 8080 externally, mapping to port 80 internally. So to access the site in your browser navigate to http://localhost:8080. The next thing the container does is mount two volumes. The first is the source code for your application, the second is the default.conf nginx file written above.

The MySQL container has port 3306 count to the host, allowing access from a MySQL management tool such as MySQL Workbench, DataGrip or DBeaver. You absolutely should not run this on a production server without firewalling it. Infact this whole environment is designed for local development, but this particularly needs raised as a point for anyone adapting this for production. Do not expose MySQL to the world! Other settings of interest here are the MYSQL_ segments. You can use these to define your username, password, database name. Additionally, the configuration mounts a volume to the MySQL database directory which means the data will be persistent until the volume is deleted. You can optionally remove this if you want volatile data that’s deleted on container restart.

The PHP container’s name is important. This relates to the nginx configuration file, where the fast_cgi parameters was defined. If you change the container definition form php: to something else, you’ll need to update it in the nginx default.conf as well as elsewhere in this file. The PHP image also needs to have a volume for the source code, and this needs to be the same path as the nginx container. Because this is a custom docker file, this needs built by docker-compose instead of just pulling an image. You can of course create this image and upload it to somewhere like dockerhub and include it from there, but I like to keep the environment customisable without messing around with external docker hubs.

The other containers are entirely optional. If you’re not running Horizon, then just remove or comment out that block. Same with the other remaining containers.

Next thing to do is create a new Laravel install in the src directory, or copy in an existing Laravel repo. Generally I install a new Laravel instance using composer like this:
`

Now all that’s left to do is run docker-compose up -d. It’ll build the PHP image, pull the MySQL and nginx image and start your containers using the ports specified in the docker-compose file. To run composer or artisan commands, simply run docker-compose exec php bash and you’ll be dropped into the web directory on the PHP docker container. From here you can easily run commands such as php artisan key:generate, php artisan migrate and any of the php artisan make: commands.

It’s also possible to version control your src folder. Do this from the host, and not inside a docker container. cd src to go into the source code directory, as it’d be unusual for you to store your dev environment with the application. git init should initialise a new git repository for you to manage as you see fit.

So here's my second annual (as in once-per-year) blog post. I hoped this would've happened more often, but oh well.

After many years of Sublime Text usage, I recently switched to VS Code, as the Sublime Text ecosystem for PHP development seems to be somewhat less active lately, based on my personal experience.

I was also recently starting work on a new Laravel project and decided to setup Xdebug so I could, well, debug. Most of the online guides I've found while hoping for a quick copy and paste configuration didn't end up working, and were in fact aimed at Xdebug 2 - whereas the new Xdebug 3 version changed some of the configuration setting keys. I spent a couple of hours getting everything to work nicely, and encountered a weird (but in hindsight, sensible) issue.

Xdebug

'Running in Docker' is not very specific, so I'll start by explaining my project setup. It's a Laravel app with a docker-compose.yml file in the project root, which looks a bit like this (documentation, unrelated services, and unrelated configuration settings removed for readability):

The HOST_UID and HOST_GID are configured to be used by Apache in the web image and PHP in the php image so as not to mess up file permissions and ownership on the host machine.

Xdebug php docker

For those curious, I also have the following services as a starting point for most Laravel projects:

  • a MySQL service as the app's database
  • a queue worker service (based on the same Docker image as the php service) for running artisan queue:work
  • a Node service for Node-related stuff (Yarn dependencies and the front-end build process)
  • Mailhog for local email testing
  • Beanstalk as a messaging queue
  • Beanstalk Aurora as a Beanstalk UI

Both the web and php services run a similar Docker image in terms of PHP - with the difference being that web also includes Apache through which the web application is served. I also have a local Nginx setup for proxying requests to various projects so that I can use https://project.localhost, https://mailhog.project.localhost etc. instead of having to remember the per-project ports for individual exposed services. For SSL in local development I use mkcert, though I guess none of this is too relevant for today's topic of Xdebug, and I should just make a separate blog post documenting that whole setup if anyone's interested.

Importantly though, the Docker images for running the web-app and CLI commands are based on ubuntu:bionic - I've been wanting to switch to the php images but just haven't gotten around to it yet. The Dockerfiles for the web and php images install PHP 7.4 from ppa:ondrej/php, along with a bunch of extensions including php7.4-xdebug - which, as of recently, ends up with Xdebug 3.0.1 installed in the image.

This whole guide should work just as well for PHP 8.0 as well as for older versions, though you probably shouldn't be using older versions anyway.

Phpstorm

If you have a very specific setup that's similar to mine, you're gonna want to do the following:

Variables order

In my Docker images, PHP's default variables order was set to GPCS - which is not good because Xdebug looks for an environment variable in $_ENV when attempting to detect a trigger from a CLI command. Thus, my Dockerfile copies a file with the following contents to /etc/php/7.4/cli/conf.d/99-variables-order.ini:

This was giving me trouble and actually took me more time than everything else here to figure out why Xdebug wasn't working for me when attempting to trigger it from the command line.

Xdebug configuration

The Dockerfile also copies a file with the following contents to /etc/php/7.4/cli/conf.d/99-xdebug.ini (and /etc/php/7.4/apache2/conf.d/99-xdebug.ini in the web image):

The first line configures step debugging, and also affects how some other Xdebug configuration works by default (notably the default value of the xdebug.start_with_request setting).

The second line tells Xdebug which address to use to connect to the IDE - which is running on the host machine, and host.docker.internal is a special hostname which resolves to the host machine's IP address.

Laravel docker debug vscode

Note that my Dockerfile configuration which installs php7.4 and (among others) php7.4-xdebug from ppa:ondrej/php using apt will automatically enable the extension as well, so I don't need to explicitly do that. If you do, you'll want to also add zend_extension=/path/to/xdebug.so in this file.

host.docker.internal

As a sidenote, this wasn't supported on Linux at all for a long time until this PR was merged - and with it, Linux users still have to explicitly map host.docker.internal to the magic host-gateway value, which Docker then resolves to the host machine's IP address when starting the container and saves it in the container's /etc/hosts file. That's why the extra_hosts setting is needed in the docker-compose.yml file - and hopefully this shouldn't cause any issues in non-Linux (ie. Mac or Windows) Docker environments, though I haven't been personally able to test it yet.

VS Code

In VS Code you want to install the felixfbecker.php-debug extension (VS Code Marketplace, GitHub):

You can follow the VS Code configuration section of that extension's installation instructions, but what worked for my setup was to add the following into my project's project.code-workspace file:

The extension also suggests adding a 'Launch currently open script' launch configuration, but I don't see how that would be useful in the context of a framework like Laravel, so I skipped it.

Laravel Docker Debug Vscode

As for the other stuff, here's what the configuration settings mean:

  • name - just the name of the configuration which will be displayed in the debugger and the taskbar
  • type - should be php to tell VS Code that this configuration should run debugging with the PHP Debug extension we just installed
  • request - should be launch as that's appropriate for how we debug PHP with Xdebug
  • port - should be 9003 for Xdebug 3, which is the new default port that Xdebug will connect to - in older versions, the default port vas 9000, but we want to minimize our configuration work, so best to stick with the defaults when we can
  • pathMappings - this is important because Xdebug is running in the Docker container, where the app's files are under a different path; this is very configuration-specific so adjust it to your own setup, but my Dockerfiles use /opt as the WORKDIR and that's the volume I bind my project's directory to - hence my value for this configuration setting
  • ignore - optional paths that errors will be ignored from
  • xDebugSettings - consult the extension's documentation for more info, but I needed to set this higher than the default values because the defaults were too small for my use-case

To expand on the last point, one issue I had with the low default xDebugSettings was that my debugger would show, for example, that $_SERVER is an array() with ~60ish items, but when I expanded that variable, it would always only show the first 32 items and nothing else. Increasing these values fixed the problem.

Xdebug Php Docker

So now we're done with configuring Xdebug and VS Code, so how do we actually debug?

There's two contexts in which it makes sense to debug PHP code - browser requests and CLI commands. Both are explained in Xdebug's documentation, but here's a short summary.

Browser

You want to install one (or more) of the following extensions, depending on which browser you're working with:

  • Xdebug Helper for Firefox (source)
  • Xdebug Helper for Chrome (source)
  • XDebugToggle for Safari (source)

Once installed, you'll get an extension icon/button in your browser which enables you to select the Xdebug feature you want to trigger. I'm only interested in debugging, so when I want to do that, I'll click the icon and select the 'debug' mode with the green bug icon.

Then I'll start a debugging session in VS Code by pressing F5 (which should be the default keybinding - if not, you can always open your 'Run' panel in VS Code and click the little 'play' icon next to the dropdown menu where 'Listen for XDebug' should be selected).

I'll add a breakpoint where I need it (by simply clicking next to the line number), and refresh my browser page. What should happen is that Xdebug will connect to VS Code, which will in turn inform Xdebug about the breakpoint which was set, and then while the code is executing, if it reaches that line of code, it will pause execution and you can then step-debug in VS Code. Yay!

Note that depending on how the web server serving your PHP application is configured, you might get timeouts in your browser - but this article won't deal with that as it's already getting too long.

Don't forget to switch the browser extension's Xdebug mode back to 'Disable' (gray bug icon) after you're done with it, to avoid invoking Xdebug on every request and slowing down performance.

Command Line

As with the browser-based flow, this again requires you to first start a debugging session in VS Code and set a breakpoint on a line of code.

To trigger Xdebug when running command-line applications (such as when unit testing or running an Artisan command), you need to configure a specific environment variable.

As I'm working with Docker Compose, I would usually run unit tests like this:

The environment variable you need to set is XDEBUG_SESSION and with the Xdebug configuration described above, it can be set to any value, as long as it's set. Xdebug's documentation suggests XDEBUG_SESSION=1 so we'll go with that:

That's it - when the code reaches a line where you've set a breakpoint, it should pause execution and focus that line within VS Code, where you can proceed to step through the code and track what's going on.

Conclusion

Laravel Docker Debug

While this might seem like a long post compared to the actual amount of work that needs to be done to just configure Xdebug and VS Code and start debugging, I was attempting to describe my environment and setup in more detail so it can be more helpful to anyone running a similar setup - as well as trying to explain why all the required configuration is required in the first place, and what it does.

Docker Php Fpm Xdebug

Hopefully this will work for you, but if it doesn't, feel free to let me know and I'll try to help out.

Stay healthy!