VS Code, WordPress, WSL2 & Docker

This blog post is going to detail my journey with running a Node & PHP (WordPress) development environment on Windows 10 using WSl2, Docker and VS Code. I will share the bare config in a final, separate post at the end.

The Goal

I am a React developer by trade, so all I really need to be happy is VS Code and Node. As part of a previous project I entered the WordPress realm and authored several WPGraphQL integration plugins. Embarrassingly, I did all of this work with a deployed WordPress installation and vanilla VS Code. I didn’t want the hassle of installing PHP and XDebug on my personal machine. I even used FTP to deploy my code changes to the server – gross.

So, having used VS Code remoting and Docker at work I felt this was a winning solution. However I do not have the time to learn how to build a Docker container development environment from scratch. Cue the Docker for Windows, WSL2 and VS Code integration. This is perfect!

The requirements:

  • VS Code should open my working directory in a Docker container
  • PHP intellisense and XDebug should be available
  • Node should run the client app from the container
  • Everything should shut down with VS Code to free system resources for gaming

And I finally succeeded!

Step One: Get environment ready

I followed the official Microsoft guide for getting this pre-release environment installed. Most of the beta channel builds have filtered down since March 2020 so you might already be ready to go.

Step Two: Docker configuration

I use a mixture of Docker Compose and a Dockerfile to build the required system.

The Docker Compose configuration does a few things.

  1. Declares www service for running WordPress and Node
  2. Declares db service for running MySQL 5.7
  3. Uses a .env file to preload MySQL and WordPress configuration options as environmental variables
  4. Mounts a delegated volume under wp-content/plugins/my-plugin so my work is available in WordPress
  5. Defines a Docker Volume for persisting the database files and mounts it in /var/lib/mysql

The Dockerfile mainly installs WordPress and XDebug. This was mostly lifted from the XDebug for Docker image.

I reopened my folder in the remote container and watched my first Docker containers building. I navigated to localhost and saw the WordPress set up page – woo!

Step Three: WordPress dependencies

My React app relies on a few dependent WordPress plugins – mainly Meta Box and WPGraphQL (and of course my integration plugins!). I contribute to all of these, so I want to mount my local repositories in wp-content/plugins. I did not want to mount these in a Docker volume as it was important to manage the files from the host.

I created a .docker directory in my repository and ignored it in .gitignore. This is where I will put anything to share between host and container. In .docker/wp-content I cloned plugins, themes and uploads required for my WordPress site to run. These were mounted as delegated volumes for performance, but this is risky as container changes aren’t persisted on the host in real-time.

Finally, I added a ‘docker blocker’ tmpfs volume mounted over wp-content/plugins/my-plugin/.docker. This will hide the .docker folder from the container. This is important for not seeing duplicate PHP files in XDebug etc. I set nocopy and a max size of 0 so I am not tempted to put anything in there.

I really should set some environmental variables and clean this up!

Step Four: Node

At this point I had a working PHP development environment. At the time I was running Node in a second VS Code window on my host, while doing PHP in my container.

To migrate my JavaScript development into my Docker container I had to overcome a few issues:

  • Create React App cannot hot reload in a container.
  • File permissions/poor performance of node_modules. This was harder to diagnose, I went through several iterations of trying to get this to work.
    • Running VS Code on a Windows directory (C:\dev\src\my-repo) would fail to compile in Docker.
    • Running VS Code from a WSL2 folder (~/src/my-repo) would compile but perform horribly.
    • Finally used a ‘docker blocker’ node_modules volume.

I added the Node Source URI to apt-get and installed nodejs in my Dockerfile.

Step Five: Polish that dev experience

At this point I have WordPress and XDebug running with their dependencies. I also can run Node and view my React app alongside the deployed WordPress version. The only thing left is to improve the developer experience a little.

Things I want:

  • Persist my bash_history
  • VS Code extensions (ESLint, IntelliPhense etc)
  • VS Code settings
  • Install a .bashrc file with some helper functions
  • Create a share directory to allow easy dragging and dropping of files into the container and vice versa

I created the .bashrc file I wanted, mostly a duplicate of my WSL .bashrc.

I saved this in my repository as .bashrc.docker and used Dockerfile COPY to sync it on container build. I also added a .gitconfig file with some defaults and COPY that to /root too.

In my .docker directory (remember this is ignored and hidden from the Docker container) I add mounts for .bash_history and share under /root.

And finally, a .devcontainer.json file for VS Code settings.

And – done!

Recap

So, once all is said and done this configuration allows me to run a custom WordPress development Docker container (with PHP, XDebug and NodeJS). You can have a custom .bashrc deployed and your command history is persisted in .bash_history.

There is a share folder mounted under /root to allow SQL dumps or other file movements on and off the container.

WordPress dependencies are kept on-host and mounted in /var/www/html/wp-content.

WordPress is forwarded on port 80 and my React app is on 3000.

ESLint is scoped to development directory to avoid surfacing linting errors in other plugins.

PHP debugging and intellisense extensions are installed on the Docker container – not your local VS Code.

And most importantly, there are no services or frameworks installed on my host, other than WSL2 and Docker for Windows. And all of these can be shut down with VS Code freeing up system resources for play time.

PowerShell move by date

I am terrible at keeping on top of my camera roll. I take pictures, they sync to the cloud, I forget them. Every time I try to sort them it is just such a huge undertaking that I give up.

Enter Move-ByDate:

Now I can tackle my camera roll one month at a time. And as a bonus if I need to grab a snap from a holiday etc, it’s probably in a small folder.

Using Restrict Content Pro with WPGraphQL

With the latest minor release of WPGraphQL comes the Model Layer – a much cleaner way to interact with post objects before they’re sent down the pipe to the client.

I use Restrict Content Pro (RCP) to handle membership levels and appropriately restrict visibility of posts to users. Previously, I was hooking into the WP Query and checking a user’s metadata for their membership and subscription info. I then used this to add a taxonomy query clause to hide posts from being returned as required.

Now, however, there is a simple hook in WPGraphQL to simply set a post as private (I am using a tutorial CPT):

Embedding dynamicly named JavaScript in WordPress

tl;dr see code snippet for a simple way to enqueue multiple JavaScript files in a WordPress template.

I have recently been doing a lot of work writing small React applications to be embedded inside a WordPress website. Clients would like a rich, interactive set of pages without sacrificing their existing site.

A simple React app built using React Scripts should be minified into three separate ES5 JavaScript files. Each build will have its own unique hash in the file name.

In the past I would rename the files and hard code their names in a wp_enqueue_scripts action handler. This soon becomes tedious, so I wrote the following.

add_action( 'wp_enqueue_scripts', 'enqueue_react_script' );
function enqueue_react_script() {
    // tutorial is my CPT name
    if ( is_singular( 'tutorial' ) || is_post_type_archive( 'tutorial' ) ) {
        // register scripts
        $directory = plugin_dir_path( __FILE__ ) . 'script';
        $iterator = new DirectoryIterator( $directory );
        foreach ( $iterator as $file ) {
            if ( pathinfo( $file, PATHINFO_EXTENSION ) === 'js' ) {
                $fullName = basename( $file );
                $name = substr( basename( $fullName ), 0, strpos( basename( $fullName ), '.' ) );
                wp_enqueue_script( $name, plugin_dir_url( __FILE__ ) . 'script/' . $fullName, null, null, true );
            }
        }
    }
}

PowerShell interactive menu

At work, I regularly need to run up to three dotnet core backend services while doing front end development in VS Code. It’s tiresome to start a new PowerShell session for each one. I’ve always wanted to write an interactive menu for PowerShell, so I finally got around to it. See below for PowerShell 6.1.

There are some things I will add to this, for example searching for launchsettings.json in my repository directory to dynamically build the form. But for now I am happy with how this turned out.

FTP upload all recently modified files

I’ve been doing some development on a WordPress site recently. Due to limitations of the hosting environment, deploying from git is not an option – I have had to FTP the changed files to the staging server to test my changes. Since I don’t know PHP I am redeploying files repeatedly with minor fixes. To streamline my uploads I wrote this small bash script (to run in my Ubuntu WSL running on Windows 10) to upload all files modified within a supplied number of minutes.

[code language=”bash”]
#!/bin/bash
HOST=ftp.host.com
USER=username@host.com
PASS=veryInsecurePassword

(
echo user $USER $PASS
echo passive
find ./wp-content/plugins/myplugin/ -mmin -$1 -not -path "*/js/*" -not -path "*/js" | while read line; do
echo "put $line"
done
echo quit
) | ftp -inv $HOST
[/code]

The magic is done in the find command. I supply the number of minutes as a parameter and this will find all files modified since that time and upload them. I exclude the js folder because it contains my source files – I have an npm script to move compiled/minified js to their forever location in my WordPress plugin.

I am going to add some clever use of environmental variables to store the time of each upload and upload all changed files since then – should fix any off-by-a-minute issues that I just know will happen.

Self Elevating PowerShell cmdlet

Sometimes you need to run a script as an administrator. It’s a simple enough process to start an Administrator PowerShell window, but you still need to navigate to the path required etc etc. Since there isn’t a simple sudo command for PowerShell one way to handle the permissions is to use a self-elevating script.

[code language=”powershell”]
$Principal = New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())
$AdminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator
if (-not $Principal.IsInRole($AdminRole)) {
Start-Process -FilePath powershell -ArgumentList $MyInvocation.MyCommand.Definition -Verb runas
exit
}
# add code here
[/code]

This will check if the executing user has the Administrators role and if not start an elevated process. This will trigger a UAC prompt. As such, it will not work in automated scripts – they will hang waiting for the user input.

The magic of this snippet is use the of $MyInvocation.MyCommand.Definition. This property includes a string representation of the currently executing function. $MyInvocation is an interesting variable and I would recommend reading up on it – there are many useful properties exposed.