Containers

WSL Containers (wslc) on Windows 11: Setup, Benchmark vs Docker

If your company is past 250 people, every Docker Desktop seat on a Windows laptop now has a free rival built into WSL. WSL containers, driven by the wslc CLI, ship with WSL itself. We benchmarked them against Docker Engine in a WSL Ubuntu distro on the same machine. wslc started about ten seconds faster from cold and used 339 MB of guest memory against 585 MB. Compose and the Docker API are still missing, though, and those decide more migrations than speed does.

Original content from computingforgeeks.com - post 172378

You get the WSL containers setup on Windows 11, a first container, ports, a Dockerfile build, virtiofs mounts and PostgreSQL. Then comes the wslc vs Docker benchmark on file I/O, cold start, memory, builds and database start-up. We rebuilt the whole lab on WSL 3.0.3, the current stable release, on a Windows 11 VM in November 2026, so every timing below is from that run.

Want the commands without the walkthrough? Our wslc cheat sheet lists every wslc command by task and maps each one to Docker, with a printable PDF.

How WSL containers are put together

WSL containers reuse the WSL plumbing but not the WSL distro model. Three pieces do the work, and knowing them makes the benchmark easier to read:

  • wslservice.exe: the privileged service creates a Hyper-V utility VM through the Host Compute Service, one per user session.
  • wslcsession.exe: a per-user process takes over that VM and does the pulls, mounts and port bindings with your rights instead of SYSTEM’s.
  • containerd and dockerd: a stock pair runs inside the VM, so images behave exactly as they do under Docker. We list the processes further down.

If the difference between those layers is fuzzy, our Docker vs CRI-O vs containerd breakdown covers it. The design itself is laid out in the Microsoft architecture deep dive.

In practice each session is its own VM with its own disk, so two users on one machine never see each other’s images. Our first wslc run created a session called wslc-cli-admin-victor, because we ran it from an elevated prompt. A normal prompt gets wslc-cli-<user> instead, which is a separate session with its own images, so pick one kind of shell for daily work. Its state lives under %LOCALAPPDATA%\wslc\sessions\<session>\. At the end of our 3.0.3 lab the main disk, storage.vhdx, held 1,611 MB with every image, build and volume from this guide in it, and a vhd volume sat beside it as its own file under volumes\. Deleting that folder resets a broken session, but it also destroys every image, container and volume in it. Run wslc system session terminate first, and only do it when you mean it.

Networking is new as well. Sessions skip the NAT and mirrored modes that WSL distros use and run a user-mode stack Microsoft calls Consommé. The VM’s Ethernet frames go over a virtio queue to a Windows process running as you, which answers DNS and routes TCP and UDP. Container traffic therefore leaves the machine as ordinary traffic from your user, so VPN clients and host firewalls treat it like any other app. We’d expect fewer “works off VPN, breaks on VPN” tickets than with classic WSL, although our lab had no VPN to prove it.

Install WSL containers on Windows 11

The container feature needs WSL 2.9.3 or later. WSL 3.0.1 was the first stable release without the preview label, and 3.0.3 is the stable build today, with 3.0.4 and 3.0.5 still marked as prereleases. We kept the test machine plain, so you can map the numbers onto an ordinary developer laptop.

  • Windows 11 Enterprise 24H2 (build 26100): a fresh install with neither WSL nor the Virtual Machine Platform feature enabled, the state most corporate images ship in.
  • 4 vCPUs and 12 GB of RAM: the container VM takes roughly half the host’s memory by default, so 16 GB is the realistic floor for a laptop that also runs an IDE and a browser.
  • Nested virtualization: our Windows 11 machine is itself a VM on Proxmox with the host CPU passed through, which costs some speed on anything that crosses the Windows boundary. Bare metal will be faster in absolute terms.
  • Docker Engine 29.9.3 in an Ubuntu 26.04.1 distro: the comparison baseline, installed from the Docker apt repository inside WSL rather than Docker Desktop, so both runtimes share the same kernel family and the same disk.

If your machine already runs WSL from the Microsoft Store, wsl --update is the documented upgrade path. On a fresh image the inbox wsl.exe is only a stub that answers “The Windows Subsystem for Linux is not installed”. So we followed the route we’d script for a fleet or a golden image:

  1. Enable the two Windows features from an elevated PowerShell prompt.
  2. Download the MSI from the GitHub release and check its hash.
  3. Install it silently, then reboot once.

Start with the features. Both commands need an elevated prompt, and neither takes effect until the reboot in step 3:

dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

Both report “The operation completed successfully.” Next, download the 367 MB package with the curl.exe that ships with Windows 11, which is far faster than Invoke-WebRequest on large files. Check the hash against the one GitHub lists for the asset before you install anything.

curl.exe -sSL -o "$env:TEMP\wsl.msi" https://github.com/microsoft/WSL/releases/download/3.0.3/wsl.3.0.3.0.x64.msi # pin: tested release, newer MSIs at https://github.com/microsoft/WSL/releases
(Get-FileHash "$env:TEMP\wsl.msi" -Algorithm SHA256).Hash

Ours printed AE977286974255BD26BF925F00F40F5692C6B2084155CBC0563C09AEA091F211, matching the published digest for wsl.3.0.3.0.x64.msi. The download took 30 seconds on our link. Install it silently, then reboot so the Virtual Machine Platform feature is active:

Start-Process msiexec.exe -ArgumentList "/i `"$env:TEMP\wsl.msi`" /qn /norestart" -Wait
Restart-Computer

The MSI finished in 34 seconds and dropped two front ends, wslc.exe and container.exe, into C:\Program Files\WSL. You do not need a Linux distro installed for any of this. wslc never touches wsl -l, and its VM is separate from any distro you already run, whether that is Ubuntu or the RHEL and Fedora images for WSL.

The first container and the session VM

After the reboot, open PowerShell and confirm the version (our lab shell was elevated, which only changes the session name), then run a throwaway Ubuntu container, which proves the whole chain (session VM, registry pull, runtime) in one command:

wsl --version
wslc version
wslc run --rm ubuntu:latest bash -c "cat /etc/os-release | head -2; uname -r"

Both commands report 3.0.3.0, running the 6.18.54 WSL kernel. The first run prints Image 'ubuntu:latest' not found, pulling before the familiar Docker-style layer progress, then the container output:

wsl --version showing WSL 3.0.3 and wslc running an Ubuntu 26.04.1 container in PowerShell on Windows 11

That first run took 114 seconds, because it boots a brand-new session VM, formats its disk and pulls the image. On 3.0.1 the same step took 133 seconds. The identical command a second time finished in 2.3 seconds. Put the first-run delay in your onboarding docs, otherwise someone will assume it hung. wslc system session list shows the session that now exists. wslc info adds the path to the per-user settings file in %LOCALAPPDATA%\wslc. That file only appears after you run wslc settings once.

Published ports bind to localhost by default

The first behaviour difference from Docker shows up as soon as you publish a port. Run nginx with the familiar -p flag exactly as you would under Docker:

wslc run -d -p 8080:80 --name web nginx:latest

Docker would publish on 0.0.0.0. wslc list shows 127.0.0.1:8080->80/tcp instead, netstat agrees with a single loopback listener, and curl.exe gets a 200 from nginx 1.31.8 inside the container:

wslc list showing nginx published on 127.0.0.1:8080, netstat loopback listener and curl returning HTTP 200

We think that is the right default for a developer laptop, since it stops a test database from being reachable on the coffee-shop Wi-Fi. When a phone or another machine on the LAN does need the container, say so explicitly:

wslc rm -f web
wslc run -d -p 0.0.0.0:8080:80 --name web nginx:latest

After that, netstat lists listeners on both 0.0.0.0:8080 and 127.0.0.1:8080, and you still need a Windows Firewall rule for inbound traffic from other machines. A browser on the same PC renders the page from http://localhost:8080 either way, served from inside the WSL container VM:

Welcome to nginx page in Microsoft Edge served by a WSL container on localhost port 8080

That browser capture is from our October run on 3.0.1; the page and the binding behaved the same on 3.0.3. wslc exec web nginx -v and wslc logs web work exactly as their Docker counterparts do.

A Dockerfile build with a health check

A static nginx page proves little, so we built a small Flask API with a health endpoint, the kind of service a team would actually develop on Windows. The project lives in %USERPROFILE%\src\hello-api with three files, starting with the application itself. The code commands open VS Code, which a fresh Windows 11 image does not have, so use notepad in its place if you skip that install:

mkdir "$env:USERPROFILE\src\hello-api"; cd "$env:USERPROFILE\src\hello-api"
code app.py

Paste in the application code, which returns the container hostname and a greeting read from an environment variable:

import os, socket
from flask import Flask, jsonify

app = Flask(__name__)

@app.get("/")
def index():
    return jsonify(host=socket.gethostname(), greeting=os.environ.get("GREETING", "hello"))

@app.get("/healthz")
def healthz():
    return "ok", 200

Pin Flask in requirements.txt with a single line, so rebuilds stay reproducible:

Set-Content requirements.txt 'flask==3.1.2'

Then open a Dockerfile in the same folder, since wslc build picks up a Dockerfile in the build context by default (the Microsoft tutorial uses a Containerfile):

code Dockerfile

This is the exact file we built and ran. The HEALTHCHECK uses Python’s standard library because the slim image has no curl:

FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
HEALTHCHECK --interval=10s --timeout=3s --retries=3 CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:5000/healthz')"
CMD ["python", "-m", "flask", "--app", "app", "run", "--host", "0.0.0.0"]

Build and tag the image from the project folder, which becomes the build context:

wslc build -t hello-api:1.0 .

The output is BuildKit-style, one numbered line per Dockerfile step, ending with the image name (the pip lines are trimmed here):

[1/5] FROM docker.io/library/python:3.13-slim@sha256:ae14d4ce8771606477a81446e7ef429edd6762305fc50c84507c1041fa8285d1
[2/5] WORKDIR /app
[3/5] COPY requirements.txt .
[4/5] RUN pip install --no-cache-dir -r requirements.txt
  | Successfully installed blinker-1.9.0 click-8.5.0 flask-3.1.2 itsdangerous-2.2.0 jinja2-3.1.6 markupsafe-3.0.4 werkzeug-3.1.9
[5/5] COPY app.py .
exporting to image
  | exporting layers
  | writing image sha256:25c5a425f6be7109b73fa53ef997f0e94a29e2f3b46c94398c66398f8157c74b
  | naming to docker.io/library/hello-api:1.0

That cold build took 83 seconds including the base image pull, down from 94 seconds on 3.0.1. A rebuild with nothing changed printed CACHED for every step after FROM and still took 7 seconds, most of it spent re-exporting layers. You’ll notice that in a tight edit-build loop, because Docker users expect near-instant no-op rebuilds. The BuildKit flags your pipelines rely on carry over:

  • --target and --build-arg: multi-stage builds and parameterised images work as they do with docker build.
  • --secret: BuildKit secrets reach RUN --mount=type=secret steps without landing in a layer.
  • -o type=local,dest=...: exports build output to a Windows folder, handy for compiled binaries.

Run the result with an environment variable and a published port:

wslc run -d --name api -p 5000:5000 -e GREETING=karibu hello-api:1.0

Within 35 seconds of starting, the health check had passed and wslc list showed the container as (healthy). A request from Windows came back from inside the container, with the container ID as its hostname:

wslc list showing the Flask container built from a Dockerfile reporting healthy, with curl returning JSON from the container

You can also set health checks per run with the --health-cmd family of flags, or switch them off with --no-healthcheck. --stop-timeout -1 waits forever for a graceful stop, which suits databases that flush on SIGTERM.

Windows folders mount over virtiofs

Bind mounts take plain Windows paths with no /mnt/c translation. The share uses virtiofs rather than the Plan 9 (9P) protocol that WSL distros use for /mnt/c. The second command mounts the parent folder read-only with the long-form --mount syntax and tries to write to it:

wslc run --rm -v "$env:USERPROFILE\src\hello-api:/src" ubuntu:latest bash -c "mount | grep /src; ls -la /src"
wslc run --rm --mount "type=bind,source=$env:USERPROFILE\src,target=/work,readonly" ubuntu:latest bash -c "touch /work/x"

The mount table reports drvfs on /src type virtiofs, and the kernel enforces the read-only flag. Every file shows up as root-owned with rwxrwxrwx permissions. That is normal for a Windows-backed share, but it bites when a tool in the container insists on strict key-file permissions, as SSH does.

wslc bind mount of a Windows folder showing virtiofs in the container mount table and a read-only mount rejecting writes

Microsoft says virtiofs is “about twice as fast” as Plan 9 without publishing a method or hardware, so we measured it. The results are in the benchmark below, and they are less flattering for bind mounts than the marketing suggests.

An app and its database on one network

A new session comes with the usual default networks. User-defined networks add name-based DNS between containers, which is what an app talking to its database needs. Create a network and a named volume, then start PostgreSQL on it:

wslc network create appnet
wslc volume create pgdata
wslc run -d --name db --network appnet -e POSTGRES_PASSWORD=labpass -v pgdata:/var/lib/postgresql postgres:18

Attach the already running API container to the same network, resolve the database by name from inside it, and ask PostgreSQL for its version:

wslc network connect appnet api
wslc exec api python -c "import socket; print(socket.gethostbyname('db'))"
wslc exec db psql -U postgres -tAc "select version()"
wslc stats

The name resolved to 172.18.0.2 on the new bridge, and the server answered as PostgreSQL 18.6:

wslc network connect with container name DNS, a PostgreSQL 18.6 version query and wslc stats output

Two details from that run shape capacity planning. wslc stats reported a 5.778 GiB limit for every container on our 12 GB machine, so the session VM gets roughly half the host RAM by default, as classic WSL does. The three idle containers used under 100 MiB of it together. PostgreSQL was ready about 20 seconds after wslc run returned (we started polling pg_isready 5 seconds in, and it answered 15 seconds later). The benchmark below times start to ready properly on fresh volumes. The default volume driver is guest, which keeps data inside the session’s VHDX. If you want a hard size cap, wslc volume create --driver vhd -o SizeBytes=... gives a volume its own ext4 VHD.

Copying files out of a running container works, but only under the container noun on 3.0.3. This pulled the app source back to Windows:

wslc container cp api:/app/app.py "$env:USERPROFILE\app-copy.py"

The command printed nothing at all, neither a byte count nor a path, and the 252-byte file was there afterwards. List the target folder before a script relies on the copy.

File I/O benchmark: wslc vs Docker in WSL

Bind-mount speed decides how a Windows dev loop feels, because that is where your source tree, node_modules and build output end up. So we ran one script through seven storage paths on the same VM. Three were wslc paths (a virtiofs bind mount, the container filesystem, a VHD volume). Four were the comparison: a WSL Ubuntu distro on /mnt/c (9P) and on its own ext4 disk, plus Docker Engine 29.9.3 in that distro with a /mnt/c bind mount and with overlayfs. The script creates, stats, reads and deletes 2,000 small files, then writes and reads a 256 MB file with dd. Each figure is the median of three runs, in milliseconds.

Storage pathCreate 2,000 filesStatReadDeleteWrite 256 MBRead 256 MB
wslc bind mount of C:\ (virtiofs)25,28310,29325,61412,0045,8542,820
WSL distro /mnt/c (9P)64,61227,73984,01218,89030,86116,995
Docker in WSL, /mnt/c bind mount (9P)68,24027,353107,87719,55028,68015,352
wslc container filesystem541711441013,48083
wslc VHD volume (ext4)19386107602,94967
WSL distro ext4 disk466104154763,08784
Docker in WSL, overlay filesystem33077121912,88390

Microsoft’s claim holds up, and then some. The virtiofs bind mount beat 9P on every operation. File creation was 2.6 times faster and small-file reads 3.3 times faster. The sequential write was 5.3 times faster and the sequential read 6 times faster (91 MB/s against 15 MB/s). The Docker /mnt/c bind mount landed close to the bare distro, which tells you the bottleneck is 9P itself and not the container runtime. Against our October run on 3.0.1, the virtiofs medians moved both ways (file creation 16% slower within an overlapping range, the sequential write 26% faster), so we see no consistent change in bind-mount speed.

The more useful number is the gap between any Windows-backed mount and storage inside the VM. Creating the 2,000 files took 25.3 seconds on the virtiofs mount and 0.19 seconds on a wslc VHD volume, about 130 times slower. Reading them back was about 240 times slower. On VM-local storage, wslc and Docker are effectively tied, with every small-file figure under 600 ms. So one layout rule matters more than your choice of runtime.

  • Source code on the bind mount: your editor on Windows needs it there, and a few thousand source files are tolerable at virtiofs speed.
  • Dependencies on a named or VHD volume: node_modules, .venv, target/ and package caches are the tens of thousands of small files that turn a 2-second install into a several-minute one on any Windows-backed mount.
  • Databases on a volume, never a bind mount: the same small-file and fsync pattern applies, and a VHD volume adds a hard size cap so a runaway table cannot fill the session disk.

Three caveats keep these numbers honest. Our Windows 11 machine is itself a VM on Proxmox, so every operation that crosses the Windows boundary pays for extra nested VM exits. Expect lower absolute times on bare metal, with similar ratios. The 256 MB reads on VM-local storage came from the page cache, so treat them as a ceiling rather than disk speed. Spread was also wide on the Windows-backed paths, with virtiofs file creation ranging from 23.0 to 30.5 seconds. Warm the session and take several runs before you trust your own figures. The script is short enough to rerun on your hardware, so save it in a scratch folder under your profile:

mkdir "$env:USERPROFILE\bench"; code "$env:USERPROFILE\bench\io.sh"

This is the exact script behind the table. It takes a directory, a label, and optionally a file count and size in MB, then prints one CSV line of timings:

#!/bin/bash
# usage: io.sh <dir> <label> [files] [MB]
D=$1; L=$2; N=${3:-2000}; MB=${4:-256}; cd "$D" || exit 1
ms(){ echo $(( ($(date +%s%N) - $1) / 1000000 )); }
T=t-$L; B=big-$L.bin; rm -rf $T $B; mkdir $T
s=$(date +%s%N); for i in $(seq 1 $N); do echo "$i" > $T/f$i; done; c=$(ms $s)
s=$(date +%s%N); find $T -type f | xargs stat -c %s > /dev/null; st=$(ms $s)
s=$(date +%s%N); cat $T/* > /dev/null; r=$(ms $s)
s=$(date +%s%N); rm -rf $T; dl=$(ms $s)
s=$(date +%s%N); dd if=/dev/zero of=$B bs=1M count=$MB conv=fsync status=none; w=$(ms $s)
s=$(date +%s%N); dd if=$B of=/dev/null bs=1M status=none; sr=$(ms $s)
rm -f $B
echo "$L,files=$N,create_ms=$c,stat_ms=$st,read_ms=$r,delete_ms=$dl,write${MB}M_ms=$w,read${MB}M_ms=$sr"

Save it with LF line endings (click CRLF in the VS Code status bar to switch), because a CRLF copy fails in bash with $'\r': command not found. Then run it once per storage path, for example against a bind mount:

wslc run --rm -v "$env:USERPROFILE\bench:/b" ubuntu:latest bash /b/io.sh /b wslc-virtiofs

Swap the mount and the label for each path you want to compare, and run each one three times, because the first pass after a fresh session is the slowest by far.

Cold start, memory and build times against Docker

File I/O is only half the daily experience, so we also timed what you notice when you open the laptop in the morning. Cold start means both VMs fully stopped, with the image already cached, then run --rm ubuntu:latest true. Times are the median of three runs from PowerShell’s Measure-Command, with the range in brackets. Memory is one reading of free -m inside each VM, 90 seconds after starting an idle nginx container.

Testwslc 3.0.3Docker 29.9.3 in WSL Ubuntu
Cold start to first container exit25.3 s (24.4 to 27.0)35.6 s (35.3 to 36.5)
Next container, VM already running4.8 s (2.2 to 5.1)7.3 s (6.3 to 8.6)
Guest memory in use, one idle nginx container339 MB585 MB
Flask image build with --no-cache, context on C:\73.3 s (41.5 to 78.2)107.8 s (100.0 to 152.1)
PostgreSQL 18 start to ready, fresh volume30.7 s (30.6 to 45.0)76.7 s (73.7 to 89.6)

wslc comes up about ten seconds faster from cold, the same margin we measured on 3.0.1. It boots a purpose-built VM with only the container stack, while the Docker route boots a full Ubuntu userland with systemd before dockerd starts. That leaner guest also explains the memory gap. On 3.0.3 wslc won the warm launch too, which Docker had edged in October.

Read the last two rows with care. Docker’s build reads its context from C:\ through /mnt/c, so part of its 35-second deficit is the 9P penalty from the I/O table, not BuildKit. The PostgreSQL row is the noisiest. Our harness polls pg_isready once a second, and on the Docker side every poll goes through wsl.exe, which adds its own start-up cost. Both runtimes were also slower than in October, when wslc took about 12 seconds and Docker 12 to 21. Our Docker distro also logged “Failed to start the systemd user session for ‘root'” on several wsl.exe calls, which may add to its side of this row and the cold-start row. Treat that row as “wslc was not slower”, not as a 2.5 times win.

The similarity inside the VM is not a coincidence. wslc system session run executes a command directly in the session VM, and filtering kernel threads out of its process list shows what the CLI is driving:

wslc system session run sh -c "ps -eo pid,comm,args | grep -v '\['"

Microsoft’s networking and port-relay helpers run next to a stock containerd and dockerd pair:

  PID COMMAND         COMMAND
    1 init            /init
   95 GnsEngine       /gns --socket 6
   96 PortRelay       /init
  104 containerd      /usr/bin/containerd --address /run/containerd/containerd.sock --root /var/lib/docker/containerd/daemon --state /run/docker/containerd/daemon
  105 dockerd         /usr/bin/dockerd --containerd /run/containerd/containerd.sock
  316 ps              ps -eo pid,comm,args

So WSL containers are the Moby engine in a VM that Microsoft builds, boots and patches. A Windows-native CLI fronts it and deliberately keeps the Docker socket away from Windows. That explains the Docker-shaped pull output and the Docker-style JSON from wslc inspect. It also explains why most missing pieces sit on the client side. Compose, DOCKER_HOST and Go templates belong to the Docker client and API, not the engine, with multi-platform builds as the exception.

Docker commands that still fail in wslc

The wslc help text lists 35 commands and most take the Docker flags. The gaps below are the ones that decide whether an existing project moves over, and each heading is the exact message wslc 3.0.3 printed. Every error is followed by the command’s usage block, which helps at a prompt and clutters CI logs.

wslc errors for the platform flag, inspect format and stats no-stream, plus docker not recognized in PowerShell

The screenshot covers the flag errors and the missing docker command. The compose and cp errors print the same way, followed by the full 35-command list, and each one is explained below with the workaround we used.

Unrecognized command: ‘compose’

There is no Compose support in any form. You get no wslc compose, and no Docker Engine API socket for the standalone docker-compose binary to talk to. A project whose README starts with docker compose up needs a scripted run of wslc network create and wslc run lines, as the wslc compose section of our cheat sheet shows, or it stays on Docker. Beyond two or three services, that is the biggest reason to stay on Docker for now.

Option name was not recognized for the current command: ‘–platform’

Both wslc run and wslc build reject --platform, which rules out QEMU emulation of arm64 images on x64 and any multi-arch build. Teams that build images for Graviton or Ampere nodes from Windows laptops should keep that job in CI.

Unrecognized command: ‘cp’

Docker accepts docker cp at the top level, but wslc 3.0.3 only has wslc container cp. The 3.0.4 build on the preview channel adds the top-level alias in PR 41902, so this error goes away once that build reaches stable. Prune follows the same pattern. It exists per object type (wslc container prune, wslc image prune and so on), with no top-level system prune.

Invalid format value: {{.NetworkSettings.IPAddress}} is not a recognized format type

wslc inspect --format accepts only json, so the Go-template one-liners in your Docker scripts break. Parse the output with ConvertFrom-Json in PowerShell instead, because wslc inspect returns a JSON array shaped like docker inspect. wslc stats has a smaller quirk. It rejects --no-stream, because it already prints one snapshot and exits.

The term ‘docker’ is not recognized as the name of a cmdlet

Installing WSL containers adds no docker shim and no DOCKER_HOST endpoint. Tools that drive Docker through its API (Testcontainers, Compose, many IDE plugins) can’t see wslc at all. For a brand-new Windows user, that first unknown command also took 58 seconds to fail, against 0.7 seconds the next time, so do not mistake the pause for a hang. Microsoft’s own route for Windows apps is the Microsoft.WSL.Containers NuGet package on the WSL container page. The GA announcement also lists support in the Dev Containers CLI, the VS Code Containers extension and Aspire, which we didn’t test.

Should a Docker Desktop team move to wslc?

For single-container services and Dockerfile builds, yes, and it can happen today. For Compose stacks and anything that talks to the Docker API, not yet. Performance is rarely the deciding factor. Against Docker Engine in WSL, wslc 3.0.3 tied on VM-local storage, started faster, used less memory and built our image faster from a Windows folder. What decides it is which commands your repos and tools depend on.

Cost is what makes the question worth asking. The Docker Desktop licence terms keep it free for personal use, education and companies under 250 employees and $10 million in annual revenue. Above either line, every developer needs a paid seat. The Docker pricing page lists $9 per user per month for Pro, $15 for Team (capped at 100 users) and $24 for Business on annual billing. A 200-developer organization on Business pays about $57,600 a year. wslc ships with MIT-licensed WSL, so that licence line goes to zero, and the real cost becomes migration effort in the gaps above.

Workloadwslc 3.0.3What we would run
Single service, Dockerfile builds, health checksWorks as testedwslc
App plus database on a user networkWorks, scripted with wslc runwslc for two or three services
Multi-service compose.yaml stacksNo ComposeDocker Desktop or Docker Engine in a WSL distro
Testcontainers, tools that need DOCKER_HOSTNo Docker API socketDocker
arm64 images, multi-arch buildsNo --platformDocker buildx, or build in CI
Heavy node_modules or build cachesFast on volumes, slow on bind mountswslc with dependencies on a VHD volume
Windows apps that embed Linux containersMicrosoft.WSL.Containers NuGet APIwslc
Managed fleets that restrict registriesIntune on/off switch and registry allow listwslc

The Intune row deserves more weight than its single line suggests. At general availability Microsoft added an “Allow WSL containers access” setting and a “WSL containers registry allow list” to Intune. Defender for Endpoint can also surface process, file and network activity from WSL containers. Your security team gets a managed, audited container runtime on every Windows laptop without a third-party agent, which carries more weight in a regulated shop than raw speed. If rootless containers matter more than Intune, our Podman on Windows Server with WSL2 guide covers another free route. Rancher Desktop is the free GUI option when you also need a local Kubernetes cluster.

The migration path we’d follow is gradual, in three moves:

  1. Move single-container services and image builds now: they worked unchanged in our lab, and they free the most seats.
  2. Keep Compose stacks on Docker Engine in an Ubuntu distro: our Docker CE on Ubuntu 26.04 steps are the same ones we ran inside WSL for this comparison.
  3. Revisit the last Desktop seats when wslc compose ships: Microsoft calls Compose its top feature request, but neither 3.0.4 (the wslc cp alias) nor 3.0.5 (--cap-add, --cap-drop, network create --ipv6) touches it.

Whichever runtime a project lands on, the benchmark says the same thing. Keep dependency trees on a volume, not on a Windows bind mount, because that single choice moved our small-file timings by two orders of magnitude.

Keep reading

Create a Bootable Windows 11 USB and Install Windows 11 Windows Create a Bootable Windows 11 USB and Install Windows 11 Best UI Applications for Managing Docker Containers Containers Best UI Applications for Managing Docker Containers Set Up a Samba Share on Windows 11 and Access It from Linux Networking Set Up a Samba Share on Windows 11 and Access It from Linux CapRover Docker Install on Debian 13: Deploy Your First App Containers CapRover Docker Install on Debian 13: Deploy Your First App Configure NTP Server on Windows Server 2025 / 2022 (w32tm Guide) Windows Configure NTP Server on Windows Server 2025 / 2022 (w32tm Guide) Resolve Docker “You have reached your pull rate limit” AWS Error AWS Resolve Docker “You have reached your pull rate limit” AWS Error

Leave a Comment

Press ESC to close