Automating WSL2 Developer Setup With a Bootstrap Script
A Windows developer's first day ends with WslRegisterDistribution failed with error: 0x80370102 because virtualisation was disabled, or with a working Ubuntu where docker says The command 'docker' could not be found in this WSL 2 distro because Docker Desktop's WSL integration was never enabled for it, and a repository cloned under /mnt/c/Users/... where npm install takes twelve minutes. Each step is documented somewhere; the sequence, with its reboot in the middle, is not. This page turns it into a resumable PowerShell bootstrap followed by the team's Linux setup inside WSL, as part of workstation provisioning and team dotfiles.
The target: after one script and one reboot, the developer has a WSL2 distribution with systemd, sensible memory limits, Docker available inside it, team dotfiles applied and repositories in the Linux filesystem.
Diagnostic
Run these in PowerShell to see where an existing machine stands:
$ErrorActionPreference = 'Continue'
wsl --status
wsl -l -v
Get-CimInstance Win32_Processor | Select-Object -ExpandProperty VirtualizationFirmwareEnabled
Test-Path "$env:USERPROFILE\.wslconfig"
wsl -d Ubuntu-24.04 -- bash -lc 'ps -p 1 -o comm=; command -v docker || echo "no docker"; pwd'
Expected bad output on a machine set up by hand:
Default Version: 2
NAME STATE VERSION
* Ubuntu-24.04 Running 2
True
False
init
no docker
/mnt/c/Users/ana
WSL2 works, but there is no .wslconfig (so WSL can take half of RAM), PID 1 is init rather than systemd, Docker is not available in the distribution, and the default working directory is on the Windows drive.
Root cause
WSL2 setup spans two operating systems and a reboot. Windows needs the Virtual Machine Platform and WSL features enabled, which requires hardware virtualisation in firmware and a restart before a distribution can be registered. The distribution then needs its own configuration: /etc/wsl.conf to enable systemd (needed by Docker Engine, snap and many services), and the user's .wslconfig on the Windows side to cap memory, because WSL2's default lets it take half of RAM and hold file cache long after containers stop. Docker Desktop's integration is enabled per distribution, and cloning into /mnt/c puts every file access through the 9P bridge between Windows and Linux, which is dramatically slower than the distribution's ext4 filesystem. Manual instructions tend to cover these in the order someone discovered them, not the order that works.
Resolution
- Write a resumable PowerShell bootstrap that records progress so it can continue after the reboot:
$ErrorActionPreference = 'Stop'
$state = "$env:LOCALAPPDATA\acme-bootstrap.phase"
$phase = if (Test-Path $state) { Get-Content $state } else { '1' }
if ($phase -eq '1') {
wsl --install --no-distribution
Set-Content $state '2'
Write-Host 'Reboot now, then run this script again.'
exit 0
}
if ($phase -eq '2') {
@"
[wsl2]
memory=8GB
processors=6
swap=2GB
[experimental]
autoMemoryReclaim=gradual
"@ | Set-Content "$env:USERPROFILE\.wslconfig"
wsl --install -d Ubuntu-24.04 --no-launch
winget install --id Docker.DockerDesktop --exact --accept-package-agreements --accept-source-agreements
winget install --id Microsoft.VisualStudioCode --exact --accept-package-agreements --accept-source-agreements
Set-Content $state '3'
}
if ($phase -eq '3') {
wsl -d Ubuntu-24.04 -u root -- bash -c "printf '[boot]\nsystemd=true\n[user]\ndefault=$env:USERNAME\n' > /etc/wsl.conf"
wsl --shutdown
wsl -d Ubuntu-24.04 -- bash -lc 'curl -fsSL https://raw.githubusercontent.com/acme/dotfiles/main/install-linux.sh | bash'
Set-Content $state 'done'
Write-Host 'Bootstrap complete.'
}
--no-launch registers the distribution without opening an interactive first-run prompt; the default user is created by the Linux install script. Docker Desktop's integration for the distribution is enabled in its settings (Resources → WSL integration), or by setting integratedWslDistros in its settings file.
- Make the Linux script set up the developer's workspace inside the distribution, including a repository directory in the Linux filesystem:
#!/usr/bin/env bash
set -euo pipefail
sudo apt-get update -qq
sudo apt-get install -y -qq git curl jq build-essential unzip
curl -fsSL https://mise.run | sh
sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin" init --apply acme/dotfiles
mkdir -p "$HOME/src"
grep -q 'cd ~/src' ~/.bashrc || echo '[ "$PWD" = "$HOME" ] || [ "${PWD#/mnt/c}" != "$PWD" ] && cd ~/src' >> ~/.bashrc
docker version --format 'docker ok: {{.Server.Version}}' || echo "enable WSL integration for this distro in Docker Desktop"
- Open repositories through the Linux path. In VS Code,
code ~/src/shopfrom the WSL shell opens a Remote WSL window, which runs extensions in Linux and keeps file access native.
Expected output
PS> wsl -d Ubuntu-24.04 -- bash -lc 'ps -p 1 -o comm=; docker version --format "{{.Server.Version}}"; free -g | awk "/Mem/{print \$2\" GB\"}"'
systemd
27.2.0
7 GB
$ cd ~/src/shop && time npm ci
added 1412 packages in 41s
systemd is PID 1, Docker works inside the distribution, WSL is capped at the configured memory, and dependency installs run at Linux speed in the Linux filesystem.
The npm ci timing is the clearest before-and-after number to share with the team: the same install under /mnt/c/Users/.../shop typically takes several times longer, and file-watching dev servers there often miss changes entirely. Moving repositories is the single change that most improves Windows developers' daily experience, which is why the bootstrap makes ~/src the default rather than leaving it as advice.
Prevention
Run the Linux half in CI on an Ubuntu 24.04 runner, since it is the same script, and run the PowerShell half on a Windows VM image whenever it changes.
Warn when a repository is on
/mnt/c. Add a check to the project doctor script that fails with a clear message ifpwdstarts with/mnt/.Keep
.wslconfigin the dotfiles repository as a Windows-side file the bootstrap copies, so changes such as a new memory limit reach everyone.
Platform caveats
WSL2:
wsl --installrequires Windows 10 22H2 or Windows 11 and hardware virtualisation enabled in firmware; error0x80370102means it is off. Corporate devices may also need the Hyper-V platform allowed by policy.
Docker Desktop alternatives: with Docker Engine installed directly in the distribution (no Docker Desktop), systemd must be enabled for the
dockerservice to start automatically.
VPN clients: some VPNs break WSL2 networking by changing routes; enable
networkingMode=mirroredin.wslconfigon Windows 11 23H2 or later, which shares the Windows network stack with WSL.
Apple Silicon (ARM64): not applicable; for Windows on ARM devices, use the ARM64 Ubuntu image and check that Docker images used by the project publish arm64 variants.
Rollback
Unregistering the distribution deletes it and everything inside it, so push work first. Features can be disabled from PowerShell:
$ErrorActionPreference = 'Stop'
wsl --unregister Ubuntu-24.04
Remove-Item "$env:USERPROFILE\.wslconfig" -ErrorAction SilentlyContinue
Remove-Item "$env:LOCALAPPDATA\acme-bootstrap.phase" -ErrorAction SilentlyContinue
Frequently Asked Questions
Why is everything so slow when my repository is on C:?
Files under /mnt/c are accessed through a network-like bridge between Windows and the Linux VM. Keep repositories in the Linux filesystem (for example ~/src), and open them in VS Code through Remote WSL.
Do I need systemd in WSL?
For most development stacks, yes. Docker Engine, some language servers and tools that install as services expect systemd. Enable it with [boot] systemd=true in /etc/wsl.conf and run wsl --shutdown.
How much memory should WSL get?
Enough for the development stack plus builds — often 8 GB on a 16 GB machine or 12–16 GB on a 32 GB machine. Set it explicitly in .wslconfig; the default can starve Windows applications.
Can the whole setup run without a reboot?
Not on a machine that has never had WSL enabled; enabling the virtualisation features requires a restart. A phase file lets the script resume cleanly afterwards.