Drupal on Windows That Doesn't Hurt: DDEV and WSL2
Most of the Drupal team I work with develops on Windows laptops. The project is a large Site Factory codebase: dozens of contributed and custom modules, several themes, and a vendor directory with tens of thousands of files. On that setup a fresh environment took 30 to 40 minutes to come up, and the first page load of the CMS after a cache rebuild could take ten minutes.
After the change below, bootstrapping a new environment takes about five minutes and the first CMS load is under one minute. Nothing about Drupal changed. What changed was where the files live.
The actual problem: files crossing the Windows boundary
The old setup was Docker Desktop plus DDEV, with the project checked out somewhere under C:\. Every PHP request inside the container read thousands of files through the Windows-to-Linux file sharing layer, and Drupal reads a lot of files on a cold cache.
The usual fix is Mutagen, which DDEV supports out of the box: it syncs your Windows files into the container so PHP reads them locally. Our old onboarding notes had "make sure mutagen: true is set, or DDEV will be slow" in them. On my machine it made things worse, not better: Mutagen itself slowed the whole computer down noticeably. And even where it helps, you now have a sync process to babysit and an initial sync that takes ages on a big vendor.
The better fix is to stop crossing the boundary at all. Put the project inside the WSL2 Linux filesystem, run Docker inside WSL2 too, and let your editor reach in from Windows. Then PHP reads files from a native ext4 disk and there is nothing to sync. Microsoft's own WSL best practices say the same thing: keep project files in the Linux filesystem when you work with Linux tools.
Step 1: start clean
Uninstall Docker Desktop and the old DDEV first. Leftovers from the old setup are not worth debugging. Then give WSL2 explicit limits with a .wslconfig file in your Windows home folder, so the Linux VM does not fight Windows for memory:
# C:\Users\<you>\.wslconfig
[wsl2]
memory=2GB
processors=16Tune those numbers to the machine, but don't give it less than 2 GB of RAM. For this project, 2 GB turned out to be the sweet spot. The point is to set the limits deliberately instead of letting the VM grow as it likes. If you ever need the memory back, wsl --shutdown stops the whole VM.
Then remove every existing distribution and install a fresh Ubuntu. Starting from the same clean distribution keeps everyone's machine the same.
# PowerShell, as administrator
wsl -l -v
wsl --unregister <each-old-distro>
wsl --install Ubuntu-22.04Step 2: DDEV with Docker inside WSL2
DDEV ships an installer that sets up Docker CE inside the WSL2 distribution and DDEV on both sides. No Docker Desktop, no licence questions, and one less layer between your code and the container.
# inside Ubuntu: update and bring your SSH keys over from Windows
sudo apt update && sudo apt -y upgrade
mkdir -p ~/.ssh && cp /mnt/c/Users/<you>/.ssh/* ~/.ssh/
# back in PowerShell: DDEV with Docker CE inside WSL2, no Docker Desktop
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/ddev/ddev/master/scripts/install_ddev_wsl2_docker_inside.ps1'))Step 3: move the project into Linux
Open the Ubuntu filesystem from Windows Explorer (it shows up under "Linux" in the sidebar), create ~/projects and clone the project there. If you move an existing checkout instead, delete vendor first and reinstall it from inside Linux.
Open the folder in VS Code with the WSL extension. The editor runs on Windows, but its terminal, git and language servers run inside Linux, right next to the files.
Since the files now live in Linux, Mutagen has nothing to do. Turn it off, both globally and for the project:
ddev mutagen reset
ddev config global --performance-mode=none
ddev config --performance-mode=noneStep 4: let DDEV do the boring setup
Every developer used to run the same handful of commands after ddev start. Those went into post-start hooks: forward the SSH agent, put the project's vendor/bin tools on the path, and install the latest Acquia CLI so pulling databases and files from the cloud works from inside the container.
name: drupal-platform
type: drupal
docroot: docroot
php_version: "8.1"
webserver_type: apache-fpm
database:
type: mariadb
version: "10.5"
performance_mode: none
hooks:
post-start:
- exec-host: ddev auth ssh
- exec: ln -sf /var/www/html/vendor/bin/* /usr/bin/
- exec: |
curl -OL https://github.com/acquia/cli/releases/latest/download/acli.phar
chmod +x acli.phar
sudo mv acli.phar /usr/local/bin/acli
web_environment:
- ACLI_DB_HOST=db
- ACLI_DB_USER=db
- ACLI_DB_PASSWORD=db
- ACLI_DB_NAME=dbA php.ini override with memory_limit = 2G in .ddev/php/ took care of Composer and Drush running out of memory on this codebase. For pages that need a headless browser, an extra docker-compose.chrome.yml adds a Chrome service next to the web container.
Step 5: one settings.local.php for every site
On Site Factory each site keeps its files in sites/<site id>/files. Locally you want sites/default/files. Instead of editing the path every time someone switches sites, the local settings file checks whether it is running inside DDEV:
// docroot/sites/default/settings.local.php
$siteId = 'SITE_ID'; // the folder name under sites/ on the platform
// DDEV keeps files in sites/default, the platform uses sites/<site id>
$folder = getenv('IS_DDEV_PROJECT') == 'true' ? 'default' : $siteId;
$settings['file_public_path'] = 'sites/' . $folder . '/files';
$settings['file_private_path'] = 'sites/' . $folder . '/files-private';
// local debugging
$config['system.logging']['error_level'] = 'verbose';
$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;Step 6: make the common path short
The last bit is not about performance, but it made the day feel faster. A few shell aliases inside WSL turned the "get me a fresh copy of this site" routine into one line:
# ~/.bashrc
alias start="ddev start"
alias stop="ddev stop"
alias db="ddev import-db"
alias drush="ddev drush"
alias uli="ddev drush uli"
alias cr="ddev drush cr"
alias cim="ddev drush cim -y"
alias cex="ddev drush cex -y"
alias updb="ddev drush updb -y -vvv"
alias ci="ddev composer install"
alias cu="ddev composer update"
alias co="ddev composer outdated"ci && db --file=./database.sql && cim && updb && uliTwo small habits helped too. Typing wsl in the Explorer address bar opens a Linux shell in that folder. And unpacking a site backup with tar -xf backup.tar.gz inside Linux takes a second or two, compared with minutes through the Windows extractor.
A few gotchas
- Copy your SSH keys into the Linux home directory. Keys that only exist on the Windows side lead to confusing git and
aclipermission errors. - If the project's git hooks were written for the old toolchain, they may fail inside WSL. Check them before you blame git.
- Upload directories created on the Windows side can come through with the wrong permissions. A
chmodonsites/default/filesafter the first import saves a round of "why can't Drupal write this image". - One known issue: importing a database with
ddev acli pull:databasedidn't work reliably in this setup. Downloading the backup and usingddev import-dbdid. - Line endings matter again. A shell script checked out with CRLF will fail inside Linux with an unhelpful error. Set
core.autocrlfaccordingly, or fix the file withsed -i 's/\r$//'.
Result
Bootstrapping a new environment went from 30 to 40 minutes to about five, and the first CMS load after a cache rebuild went from about ten minutes to under one. After that, admin pages open within three to four seconds at most. The whole setup is on GitHub as drupal-local-development. If your Drupal team is on Windows and someone still says "DDEV is just slow", it is probably not DDEV. It is the filesystem.