IntroductionΒ
If youβre building .NET Aspire projects on Windows, you might be getting frustrated with the performance of your Docker containers. As a cross-platform developer, I switch between Windows and macOS frequently, and Iβve noticed a significant performance gap between the two platforms. Fortunately, thereβs a simple solution to give your Docker containers a performance boost.Β
Yes and no. Iβve come across this performance discrepancy between code running on macOS and code running on Windows before, specifically in Node applications. I donβt develop them, but I do run them sometimes. For example, my website is built with Jekyll, a Ruby-based static site generator. Being able to run it locally and preview posts before I publish them is fairly important, but the performance on Windows is so bad that it makes it almost impossible. This was frustrating, especially considering how snappily it ran on my M1 MacBook. Iβve since upgraded to a newer machine, but I mention this to highlight that itβs not just a hardware issue. My Windows machine is an i9 with 32GB of RAM, but would get smoked by my 8GB M1. I didnβt think it could be purely a hardware issue, and I also figured that Node developers canβt exclusively be using macOS, so I did some digging.
While Node does feature some optimisations for ARM processors, itβs not just the hardware that makes a difference, itβs the OS too – lowish spec Linux machines can run Node without any trouble, in fact even a Raspberry Pi can run a Node server without breaking a sweat. (Note that Iβm talking about development builds – production builds donβt tend to have this problem on Windows).
Diving into the nuts and bolts of why Node has such a hard time on Windows compared to macOS and Linux is beyond the scope of this article, but itβs enough to know that Linux can run Node efficiently, and that means that my Windows hardware can at least in principle do it as well.
Fortunately, you donβt need to switch to Linux. With WSL, you can get the best of both worlds.
WSL2
If youβre reading this post, you almost certainly know what Windows Subsystem for Linux (WSL) 2 is, but in case you donβt, itβs a way to run a Linux kernel on Windows. Itβs not a hardware-emulated VM, but rather a translation layer, which provides a lightweight way to run Linux binaries on Windows. With WSL 2, you can even install your favourite Linux distro from the Microsoft Store. I tend to use Ubuntu, purely because its popularity means that itβs well supported and has a large community, so when I encounter issues, I can usually find a solution quickly.
When you set up WSL, you get access to your Windows filesystem as a mounted drive, and your Linux filesystem is accessible from Windows too. At first, I tried navigating in my bash terminal to the folder where my Node project was stored and running from there. But this would often not work at all, and when it did, it was at best still slow, and at worst riddled with countless errors. Thereβs a simple fix for this though, and thatβs to run your project from the Linux filesystem.
Instead of navigating to my repo in the Windows filesystem, I just cloned it to the WSL filesystem and ran it from there instead.
Once I did this, I got performance that was on par with macOS. Well, to be fair, I havenβt actually benchmarked these, but the difference between running natively from Windows is night and day, and to be honest Iβm not really fussed about the specifics. The point is, itβs a usable solution for me, and thatβs really all that matters. As thereβs no perceptible difference for me now between running Node on WSL 2 or macOS, I havenβt felt the need to run any benchmarks.
Docker
As with WSL, if youβre reading this then you also already know what Docker is. But an important point to note is that, like with WSL, Docker isnβt a hypervisor, and containers arenβt VMs. This means that you can potentially be limited from running certain containers on Windows, especially if theyβre Linux-based (the reverse is also true). This is where WSL 2 comes in handy, as it allows you to run Linux containers on Windows.
When you install Docker, you can choose between using WSL 2 as the backend, or the Windows Hypervisor Platform (the same service that runs Hyper-V). WSL 2 is the recommended option, by Docker and Microsoft, for good reasons which I wonβt go into here (but in short itβs much lower overhead – with WHP, youβre running a full hardware-emulated VM, with WSL 2, youβre running a lightweight Linux kernel).
Most of the time, this runs buttery smooth. As I mentioned, this is the recommended setup and usually results in pretty good performance.
.NET Aspire
I often describe .NET Aspire as βdocker compose in C#β. This is a gross oversimplification – itβsβ―wayβ―more than that, and itβs a bit of a disservice to describe it that way. But itβs also not inaccurate – .NET Aspireβ―doesβ―allow you to define your services in C#, and itβ―doesβ―run them in Docker containers.
What this means is that your Aspire services run as Docker containers, and generally they run pretty well. However, if youβre running them on Windows, you might occasionally run into some performance issues. I wouldnβt describe these as showstoppers, and in fact it was only when I switched a particular project to macOS that I noticed the difference. But itβs there, and itβs noticeable, and this became important for a client project where I needed to frequently run demos. The performance was fine for my developer experience, but some of the delays in the demos were not conducive to convincing the client that the solution was adequate.
The Fix
It turns out, you canβt just run your Aspire projects from WSL. This is a shame – I think a lot of people would like to see this. If you look on the Aspire repo, you can see itβs a popular request. But it turns out itβs not as simple as it seems -β―this issueβ―provides some insight into some of the challenges, andβ―this discussionβ―contains a link to the filtered list of WSL issues on the repo, highlighting the problems faced by developers trying to run Aspire projects from WSL.
But it turns out you can still gain performance improvements in Docker without running your Aspire project from your WSL filesystem. The fix, it turns out, is actually to limit the memory and CPU usage of WSL.β―This StackOverflow questionβ―(and the replies) provide the fix. As you can expect, a popular answer (although not the accepted one) recommends running from your Linux filesystem. This is what Iβve always done, but as mentioned, this doesnβt work for Aspire projects.
The accepted answer states that you can create aβ―.wslconfigβ―file in your user profile, shut down Docker desktop (essentially you have to make sure theβ―VMMemβ―process is not running; you may have to kill it manually), and then restart Docker. This will apply the settings in theβ―.wslconfigβ―file, which can limit the memory and CPU usage of your Docker containers. You may have to tweak it to figure out what works best for you, but for me I simply left the default of one processor, and set the RAM limit to 6GB:
[wsl2]
memory=6GB #Limits VM memory in WSL 2 to 6GB
processors=1 #Makes the WSL 2 VM use one virtual processors
Depending on your workload you may need to adjust this. I wanted to start with these and tweak as needed, but in my case I found this worked well enough.
I must admit I was sceptical, but it really does work. Again, I havenβt done any benchmarks here, but let me give you an anecdote. A solution I am currently working on for a client involves importing documents to be used as templates for reports. In my Aspire infrastructure, Iβm running the Cosmos DB emulator for persistence and Azurite for blob storage. In the UI, you can select a file for upload, and once itβs uploaded it will process in the background. Once the file has been processed, the UI updates to show the file.
On macOS, when I upload a file, it appears in the UI straight away. On Windows it would take several seconds. Since making this change, Iβve noticed a significant performance improvement. Itβs still not instant, but thatβs because Iβve introduced another step in the process (uploading to SharePoint rather than Azurite), so I will need to run it again on macOS to compare, and in this case, I may need to run the benchmarks after all!
Even so, itβs been a huge improvement and, counter-intuitive as it may be, it works and Iβm happy with the results.
Why does this work?
As I said, this seemed counter-intuitive. As others noted in response to that answer, the issue isnβt with Windows performance, itβs with container performance. I would understand if the issue was that when running Docker containers, my Windows machine was struggling to keep up, but that didnβt seem to be the case.
But it turns out that actuallyβ―wasβ―the problem. Without aβ―.wslconfigβ―file, WSL will consume all available CPU and memory (the same thing happens with SQL Server and Exchange Server, which is why theyβre not recommended to run on the same machine). As Docker runs on top of WSL, when the containers are running, WSL consumes all available resources and starves the OS. Since WSL doesnβt release memory back to Windows easily, it forces the OS to use disk-based virtual memory (swapping), which is much slower than RAM.Β
Where this sneaks under the radar for developers like me, is that while this is happening, youβre typically debugging code running in a container. When you stop, things usually go back to normal (but not always – sometimes you have to kill WSL to get it to release the resources), and you donβt notice any performance issues in your OS. This is why itβ―seemsβ―like the issue is just with Docker – and in a sense, it is, but the problemβ―doesβ―in fact impact the whole OS. I just wasnβt noticing.
Conclusion
If youβre running Aspire projects on Windows and youβre noticing performance issues, try limiting the memory and CPU usage of your Docker environment through aβ―.wslconfigβ―file. Itβs a simple fix that can make a big difference. In fact, even if youβre not running Aspire, you might find this useful for anything where youβre using Docker on WSL 2, or even anything with WSL 2 in general.Β
Iβm may be late to the party with this one, but it made a huge difference for me, and I hope it does for you too.
Do you have any other performance improvement suggestions for Aspire on Windows? Let us know in the comments below!