A (working) PHP development environment on WSL2

Brian Richardson
5 min readJan 19, 2021

If you’re like me, you’ve probably spent a lot of time trying to figure out how to leverage WSL for your PHP development environment to save you the trouble of additional virtualization and for ease of use. Ideally we’d all run Linux, but for various reasons we can’t. That gap is closing, but in the meantime this recipe for a working PHP development environment on WSL2 might make things better.

WARNING: ADVANCED CONTENT AHEAD. YOU CAN BREAK YOUR WSL DNS IF YOU’RE NOT CAREFUL!

Overview

We are aiming for the same experience on WSL as we’d get on Linux. I like the valet-linux package by cpriego as it is a good emulation of Laravel Valet on Linux (it was originally written for macOS). That said, valet-linux is a bit problematic on WSL2 due to the lack of systemd support and the split-brain personality of WSL2. However, we can overcome both obstacles and venture forth!

This solution is built on the following applications:

  • PHP 7.4
  • Laravel Valet
  • NGINX
  • dnsmasq

Installing Valet

Install composer if you haven’t already:

sudo apt install composer

Install valet-linux:

composer global install cpriego/valet-linux

valet install

Create a Sites directory somewhere convenient:

mkdir ~/Sites

Park the Sites directory with Valet

valet park

And start Valet

valet start

Well, not bad. This at least installs all the required packages and sets up the configuration. But it doesn’t work. Let’s look at why.

Need local browser

The first thing that you’ll want to do is run the browser from Linux. DNS is enough of a problem without trying to make it work on the Windows side the way you want. So, you’ll need to install a browser. I used firefox:

sudo apt install firefox

OK, great. But where will it display? Get thee an X server for Windows! (I used X410 from the Microsoft Store). Now the really fun part, what do I put in the DISPLAY variable? It turns out that the built-in DNS server that Windows uses to serve DNS to WSL can resolve a useful hostname: <machinename>.local. This will provide all the known addresses of the Windows host from linux. So we can set our DISPLAY variable thus:

export DISPLAY=<machinename>.local:0.0

Make sure you enable “public access” on your X server, and ensure Firefox starts

firefox

This is the browser you will use for debugging. I haven’t tried Chrome, but I imagine it might work as well. In either case, you will also need to install the Xdebug extension in your browser. Install from the appropriate extension store, and configure it with the IDE Key “XDEBUG_VSCODE”. That’s the client side taken care of. This extension is very useful; you can turn debugging on and off from your browser.

The server side is a little more involved. First, install Xdebug:

sudo apt install php-xdebug

We need to configure it a little too, so edit /etc/php/7.4/fpm/conf.d/20-xdebug.ini and add the following lines to it:

xdebug.remote_enable=1

xdebug.remote_autostart=1

xdebug.remote_connect_back=0

xdebug.remote_port=9001

xdebug.idekey=”XDEBUG_VSCODE”

I’ve moved the default port to port 9001 from 9000. This isn’t necessary so you can leave it at the default 9000 if you like. Ensure that the launch.json file in VS Code uses the same port.

Restart FPM:

sudo /etc/init.d/php7.4-fpm restart

Notice: I have used the init.d script and not the standard service semantics. This is because the FPM service requires a working systemd, something lacking on WSL2. However, the init.d script works, so no problem. Just remember that anytime Valet says it restarted FPM, you’ll need to do so manually.

VS Code Setup

VS Code is installed on Windows. Install the Remote-WSL extension and connect to your WSL instance. From Remote-WSL, install the PHP Extension Pack extension. Create a folder “hello” in ~/Sites and add the file index.php with the following content:

<?php

phpinfo();

?>

Open the folder hello in VS Code and put a breakpoint on the phpinfo() line.

Here’s where I should say: “visit http://hello.test and your breakpoint will trigger in VS Code”. But I don’t :(

DNS Resolution

The first problem is that hello.test doesn’t currently resolve. I know, I know. We installed Valet, and Valet installed dnsmasq. But we aren’t using it yet. Check the contents of /etc/resolv.conf. You still have the default generated resolv.conf that points at your Windows host for DNS. That’s where the magical “<machinename>.local” comes from. Let’s proceed to gut our DNS.

First thing: let’s tell WSL not to generate our resolv.conf anymore. Create the file /etc/wsl.conf, and add the following lines:

[network]

generateResolvConf=false

Now that we have resolv.conf all to ourselves, we can start by setting the nameserver to 127.0.0.1. Change /etc/resolv.conf to contain this line only:

nameserver 127.0.0.1

Now, nslookup hello.test. You should get 127.0.0.1. Woohoo!

Now, nslookup google.ca. It will fail. Oh noes!

Also, nslookup <machinename>.local. It will also fail. More noes!

OK, no need to panic, we can fix the last two. My first attempt involved putting a server line into /etc/dnsmasq.conf that pointed back at the Windows host (some bash-fu). Unfortunately, the mini DNS server that Windows runs to resolve names doesn’t support recursive queries. That means dnsmasq can’t simply forward the DNS to our Windows host :(

Fine. We’ll just have to take over the job ourselves. It’s not like it’s really that hard. We need to be able to:

  • resolve Internet addresses
  • resolve <machinename>.local
  • resolve <machinename>
  • resolve localhost

Resolve Internet Addresses

This is solved by adding a server=1.1.1.1 (or 8.8.8.8 or whatever) to /etc/dnsmasq.conf. Make sure that the server you point at will support recursive queries (most do). Restart dnsmasq (/etc/init.d/dnsmasq restart) and you should now be able to resolve google.ca.

Resolve machine aliases

We’re simply going to have to accept the split-brain personality of WSL2 at some point. I would suggest the following aliases:

  • <machinename>.windows
  • <machinename>.linux
  • <machinename>

These aliases need to go into /etc/hosts. However, the values for these are going to change everytime the WSL VM restarts. So, I modified the dnsmasq init.d script to include the following (just before exec $DAEMON…):

gwip=`ip route get default to 1.1.1.1 | head -1 | cut -d ‘ ‘ -f 3`

localip=`ifconfig | sed -n -E “s/inet (\S+)/\1/p” | grep -v “127.0.0.1" | sed -E “s/^\s+//” | cut -d ‘ ‘ -f 1`

sed -e “s/__GATEWAYIP__/$gwip/” /etc/hosts.template | sed -e “s/__LOCALIP__/$localip/” | tee hosts > /dev/null

Remember: our local ip is the address of eth0, and the Windows ip is the address of our IPv4 gateway. The script above extracts these values, and rewrites /etc/hosts with our /etc/hosts.template file, which looks like this:

127.0.0.1. localhost

__GATEWAYIP__ dev01.windows

__LOCALIP__ dev01.linux. dev01

That takes care of the rest of them. You should now have a working DNS that includes the Valet-dns resolver.

And now I can finally say: “Visit http://hello.test in firefox, enable Xdebug for the page and reload. You will hit the breakpoint in VS Code”:

--

--