WSL puts a real Ubuntu next to your Windows tools, and most .NET developers use it for one of three things: a Linux build to check before deploying, a database or service that only runs there, or Docker Desktop's engine, which lives in a WSL distribution whether you asked or not. This part installs a throwaway Ubuntu 24.04 distribution on WSL 2.7.10, gives it a user and systemd, installs the .NET SDK from Ubuntu's own feed, measures a build on the Windows drive against the Linux file system, reaches a Kestrel app in WSL from Windows, backs the whole distribution up and restores it. Nothing on the PC's real distribution changed; every output is quoted.

Steps in this part
  1. wsl --version, wsl -l -v, and Test-Path $env:USERPROFILE\.wslconfig
  2. wsl --install Ubuntu-24.04 --name ds-p2 --no-launch, then wsl -d ds-p2 -- id: you are root until a user exists
  3. Create a user, set [user] default= in /etc/wsl.conf, wsl --terminate ds-p2, check id and systemctl is-system-running
  4. sudo apt-get update && sudo apt-get install -y dotnet-sdk-10.0, then dotnet --info
  5. dotnet new web twice, under /mnt/c/... and under /home; time dotnet build in each, twice
  6. Run the app on localhost:15920 inside WSL and curl it from Windows
  7. wsl --export, wsl --import under a new name, wsl --unregister both when done

A distribution of your own, in a minute

WSL 2.7 installs a second copy of a distribution under any name you like, with no Store: wsl --install Ubuntu-24.04 --name ds-p2 --no-launch took 54 seconds and printed Distribution successfully installed. It can be launched via 'wsl.exe -d ds-p2'. Because of --no-launch the first-run prompt never ran, so id answered uid=0(root), and the image already had systemd=true in its /etc/wsl.conf. A user and a default:

[boot]
systemd=true

[user]
default=maria

wsl --terminate ds-p2 and the next command ran as maria with systemctl is-system-running answering running. The documentation says to use wsl --shutdown for systemd changes; terminating the one distribution was enough here, and it matters, because wsl --shutdown stops every distribution, Docker Desktop's engine included. The users and passwords post has the rest of that story, including wsl -u root, which worked here with no password and is the way back in when sudo's is forgotten.

The .NET SDK inside WSL

Microsoft's Ubuntu page now says ".NET is available in the Ubuntu package manager feeds. The Microsoft package repository no longer contains .NET packages for Ubuntu", and its table lists 10.0 in the built-in feed for 24.04. So the install is Ubuntu's own:

sudo apt-get update && sudo apt-get install -y dotnet-sdk-10.0

Thirty-three seconds, 164 MB downloaded, 624 MB on disk, and dotnet --info reported SDK 10.0.112 on runtime 10.0.12 under /usr/lib/dotnet. Note the feature band: the Ubuntu feed ships the 10.0.1xx SDK, while the Windows side of this PC runs 10.0.402. A global.json pinned to the Windows version with no roll-forward will not resolve inside WSL, which is Part 1's lesson arriving from the other direction. And the first dotnet new printed something Part 6 relies on: "Installed an ASP.NET Core HTTPS development certificate."

Paths in both directions

From Linux, the Windows drives are under /mnt/c, mounted over 9p with your user id, so every file there looks like yours with mode 777. From Windows, the distribution is a UNC path: \\wsl.localhost\ds-p2\home\maria, and the older \\wsl$ form still works. wslpath translates: wslpath -w /home/maria printed \\wsl.localhost\ds-p2\home\maria, and the other way round has a trap:

wsl -d ds-p2 -- wslpath -u 'C:\Users'
wslpath: C:Users

The command after -- goes through the distribution's shell, which eats the backslash. wsl -d ds-p2 --exec wslpath -u 'C:\Users', a doubled backslash, or forward slashes all printed /mnt/c/Users. A session started from a Windows folder begins in that folder as /mnt/c/..., not in the Linux home.

Where the files are decides the build time

The same empty web project was built with time dotnet build -nologo -v q inside the distribution, once under /mnt/c and once under /home/maria, twice each; then again with fresh copies, the Linux side first, the build server shut down between rounds:

LocationFirst buildWarm buildOrder check, three builds
/mnt/c, the Windows drive over 9p7.2 s3.8 s8.2, 4.9, 3.8 s
/home/maria, ext45.9 s1.4 s6.5, 3.1, 1.8 s

The Linux file system won every pairing in both orders, by about two to one on the warm build of a two-file project; a real solution has thousands of files to stat. The documentation's advice is the same: "For the fastest performance speed, store your files in the WSL file system if you are working in a Linux command line". Keep the repository where the tools that build it live, and clone it twice if you build on both sides.

A server in WSL, reached from Windows

dotnet run --urls http://localhost:15920 inside the distribution, and from Windows curl.exe -sS -D - -o NUL http://localhost:15920/ answered HTTP/1.1 200 OK on localhost, on 127.0.0.1 and on [::1]: WSL's localhost forwarding, on by default, carries a port bound inside the distribution to the Windows loopback. The app kept the distribution alive past the 15-second idle timeout as long as it ran. One thing bit the run: starting the server in the background from a single wsl -- bash -c call with a trailing & lost the process every time, before it even created its log file. Detaching it kept it alive:

wsl -d ds-p2 -- bash -lc "cd ~/WslLab && setsid -f nohup dotnet run --urls http://localhost:15920 > /tmp/web.log 2>&1 < /dev/null"

Stop it with wsl -d ds-p2 --exec pkill -f WslLab; the --exec matters, because inside bash -c the pattern would match the bash itself. After the stop, Windows curl reported an empty reply rather than a refusal for a moment, the forwarding side closing last.

Memory, processors, and a clock set to UTC

With no .wslconfig on this PC, the distribution saw all 24 processors and 15 GiB of the 31.8 GB, the documented defaults of "The same number of logical processors on Windows" and "50% of total memory on Windows". Those two keys live in %UserProfile%\.wslconfig and apply to the one WSL 2 virtual machine that every distribution shares, Docker Desktop's included; there is no per-distribution memory setting, and the file "must be created" since it does not exist by default. A side finding worth a line: the fresh distribution's clock said UTC on a PC set to India Standard Time, because WSL asks for the legacy zone name Asia/Calcutta, which Ubuntu 24.04 keeps in the tzdata-legacy package; installing that package and terminating the distribution fixed date. Until then, every log inside WSL carries the wrong hour.

Backup: export and import

wsl --export ds-p2 ds-p2.tar wrote 2.51 GB in 48 seconds; wsl --import ds-p2-copy from that tar took 18 seconds, and the copy started as maria, not root, because /etc/wsl.conf travelled inside the tar with its [user] section. wsl --unregister removed the copy and its folder in a fifth of a second. That pair is the whole backup and restore story for a distribution, and the reason a wsl.conf belongs in every distribution you care about.

Frequently asked

How do I install the .NET SDK in WSL Ubuntu?
On Ubuntu 24.04, from Ubuntu's own feed: sudo apt-get update and sudo apt-get install -y dotnet-sdk-10.0; Microsoft's package repository no longer carries .NET for Ubuntu. This installs the 10.0.1xx feature band under /usr/lib/dotnet, so a global.json pinned to a Windows 10.0.4xx SDK needs rollForward latestFeature to resolve inside WSL.
Should I keep my project on /mnt/c or in the WSL file system?
In the file system of the tools that build it. Inside WSL, a build of the same project under /home was about twice as fast as under /mnt/c on the warm build, in both orders tested, because /mnt/c is a 9p share of the Windows drive. The documentation says the same: store files in the WSL file system when working from a Linux command line.
How do I reach a web app running in WSL from Windows?
On localhost: WSL's localhost forwarding is on by default, so an app bound to localhost:15920 inside the distribution answers at http://localhost:15920 from Windows, on 127.0.0.1 and [::1] too. Start it detached with setsid -f if you launch it from a single wsl command, or it dies when that command returns.

Next: Part 3, ports and the firewall: who listens on what, the ranges Windows reserves, localhost against the LAN, and what the Windows Security Alert really does.