Journal
Progress
Section titled “Progress”- Understand what WSL containers is and which version it requires
- Install WSL ≥ 2.9.3 (pre-release)
-
wslc run --rm hello-world - Lesson 3: containers, ports,
exec - Lesson 4: building an image
- Run an existing service (qdrant) with
wslc - Test Compose
- Check whether Docker and
wslcimages are shared - Small C# program with
Microsoft.WSL.Containers
2026-09-12 — Taking stock
Section titled “2026-09-12 — Taking stock”- Machine: Windows 11 Pro build 26200, WSL 2.6.3.
- WSL distros already present:
docker-desktop(Docker Desktop 4.61) andpodman-machine-default. - Latest WSL versions on GitHub: stable 2.7.14, pre-release 2.9.11. Only the pre-release includes
wslc.
Incident not directly related, but instructive: Docker Desktop crashed three times within a few minutes (“wsl-bootstrap stopped with exit code 1, did wsl shutdown?”). At the same time, wsl -l -v failed with:
Not enough memory resources are available to complete this operation.Error code: Wsl/0x8007000eCommitted memory: 97 GB out of 132 GB. Hypothesis: too many VMs and heavy processes at the same time (Docker + Kubernetes, Podman, ollama, rust-analyzer…). Lead: limit WSL memory in %UserProfile%\.wslconfig (memory=24GB).
2026-09-13 — First update attempt: failure
Section titled “2026-09-13 — First update attempt: failure”wsl --update --pre-release# Updating Windows Subsystem for Linux to version: 2.9.11.The command returned with no visible error, but wsl --version still showed 2.6.3.
In the Windows log (MsiInstaller):
Error 1921. Service 'WSL Service' (WSLService) could not be stopped.Product: Windows Subsystem for Linux -- Installation failed. (status 1603)Lesson: don’t rely on the absence of an error message; check the version afterwards. The WSL service was held by Docker Desktop, Podman and several wsl.exe processes.
Planned fix: stop everything, wsl --shutdown, Stop-Service WSLService as administrator, then rerun the update. → detailed in lesson 2.
2026-09-13 — Second attempt: WSL 2.9.11 installed
Section titled “2026-09-13 — Second attempt: WSL 2.9.11 installed”Applied the planned fix. From a normal (non-elevated) terminal:
docker desktop stoppodman machine stopwsl --shutdownwsl -l -v # podman-machine-default and docker-desktop: StoppedThen, in an administrator PowerShell:
wsl --shutdownGet-Process wsl, wslhost, wslrelay -ErrorAction SilentlyContinue | Stop-Process -ForceStop-Service WSLService -Forcewsl --update --pre-releasewsl --versionWSLService: StoppedUpdating Windows Subsystem for Linux to version: 2.9.11.WSL version: 2.9.11.0Kernel version: 6.18.40.1-1WSLg version: 1.0.79This time the service really was stopped before the installer ran, and the version changed. wslc.exe is installed in C:\Program Files\WSL\, which is already on the machine PATH — but terminals opened before the update don’t see it: open a new one (or call the full path).
wslc --version # wslc 2.9.11.0wslc run --rm hello-worldImage 'hello-world' not found, pullinglatest: Pulling from library/hello-world...Status: Downloaded newer image for hello-world:latest
Hello from Docker!This message shows that your installation appears to be working correctly.Don’t be fooled: “Hello from Docker!” is just the text baked into the hello-world image. Docker Desktop was stopped the whole time; the container ran under wslc.
wslc info shows a settings file at %LocalAppData%\wslc\settings.yaml and a session named wslc-cli-<user>. wslc images lists only hello-world (10.1 kB).
Still to do: restart Docker Desktop and Podman and check that Docker Desktop 4.61 still works with WSL 2.9.11. (Docker Desktop: OK, see “Remaining open questions” below. Podman 5.8.3: podman machine start OK, podman run --rm quay.io/podman/hello OK; it warns that the default Docker API pipe is already taken by Docker Desktop and exposes its own, npipe:////./pipe/podman-machine-default.)
2026-09-13 — Reproducing hello-world by hand: two traps
Section titled “2026-09-13 — Reproducing hello-world by hand: two traps”Trap 1: wslc “not recognized” in a new tab
Section titled “Trap 1: wslc “not recognized” in a new tab”PS C:\Users\spare\source\repos> Get-Command wslcGet-Command : The term 'wslc' is not recognized as the name of a cmdlet, function, script file, or operable program.wslc.exe was installed and C:\Program Files\WSL\ was on the machine PATH, but a program receives a copy of the environment variables when it starts. Windows Terminal had been running since 2026-09-11, before the update, and the new tab inherited its old PATH.
Fixes, from quickest to most durable:
# Reload PATH in the current PowerShell (this terminal only)$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')- or call the full path:
& "C:\Program Files\WSL\wslc.exe" --version; - or close every Windows Terminal window (no
WindowsTerminal.exeleft) and restart it, or start a terminal fromWin + R.
In cmd.exe, the equivalent of Get-Command is where wslc.
Trap 2: an administrator terminal doesn’t see the same images
Section titled “Trap 2: an administrator terminal doesn’t see the same images”In a fresh cmd.exe (opened in C:\Windows\System32, a sign of an elevated terminal), wslc run --rm hello-world downloaded the image again, even though it had already been pulled from a non-elevated terminal:
Image 'hello-world' not found, pullingwslc info explains why:
Sessions: 2ID Creator PID Display Name1 144356 wslc-cli-spare2 394856 wslc-cli-admin-sparewslc creates one session per user and per privilege level: wslc-cli-<user> without elevation, wslc-cli-admin-<user> as administrator. The image pulled in one session was not found in the other.
Lesson: wslc does not need elevation. Always use a non-elevated terminal, otherwise images and containers end up in a separate session.
Verification: what is separated, and who can see what
Section titled “Verification: what is separated, and who can see what”Non-elevated terminal: create a volume and a network, then delete the image.
wslc volume create sesstest-volwslc network create sesstest-netwslc image remove hello-worldwslc images # emptyAdministrator terminal:
> wslc image listhello-world latest e2ac70e7319a 5 months ago 10.1kB> wslc volume listDRIVER VOLUME NAME> wslc network listd9c6879f7df3 bridge bridge local0dd65e6d15ec host host local34ad73ecd806 none null local- The image deleted in the non-elevated session is still there in the admin session.
- The volume and network created in the non-elevated session don’t appear in the admin session. Even the default
bridge/host/nonenetworks have different IDs: each session runs its own engine.
The global option --session goes before the subcommand (wslc --session <name> image list; after it: Option name was not recognized). Access is asymmetric:
# non-elevated → admin session> wslc --session wslc-cli-admin-spare image listThe requested operation requires elevation.Error code: ERROR_ELEVATION_REQUIRED
# administrator → non-elevated session: works> wslc --session wslc-cli-spare volume listDRIVER VOLUME NAMEguest sesstest-volOn disk, each session has its own virtual disks:
%LocalAppData%\wslc\sessions\wslc-cli-spare\storage.vhdx ~587 MB%LocalAppData%\wslc\sessions\wslc-cli-spare\swap.vhdx 36 MB%LocalAppData%\wslc\sessions\wslc-cli-admin-spare\storage.vhdx ~577 MB%LocalAppData%\wslc\sessions\wslc-cli-admin-spare\swap.vhdx 36 MBCleanup: wslc volume remove sesstest-vol, wslc network remove sesstest-net.
2026-09-13 — Limiting the CPU and memory of a wslc session
Section titled “2026-09-13 — Limiting the CPU and memory of a wslc session”wslc settings creates %LocalAppData%\wslc\settings.yaml on first run (with every setting commented out) and opens it in the default editor. The relevant part:
session: # Number of virtual CPUs allocated to the session (e.g. 4 default: all available CPUs) # cpuCount: default
# Memory limit for the session (e.g. 2GB default: half of available memory) # memorySize: defaultThe same file also has maxStorageSize (default 1 TB), storagePath (where wslc\sessions\<session>\storage.vhdx is created), defaultBindingAddress (default 127.0.0.1 for -p), hostLoopback (host.wslc.internal) and idleTimeout (30 s). Source: the generated file, https://aka.ms/wslc-settings. The API equivalent is SessionSettings.CpuCount / MemoryMB (Microsoft Learn).
Measurement. Host: 24 logical CPUs, 63.7 GB of RAM.
wslc run --rm alpine sh -c "echo nproc=`$(nproc); free -m"| Settings | nproc |
RAM (free -m) |
Swap |
|---|---|---|---|
| defaults | 24 | 31946 MB | 32617 MB |
cpuCount: 4, memorySize: 4GB |
4 | 3919 MB | 4096 MB |
Swap follows memorySize.
Trap: the change is not applied to a running session. The first measurement after editing the file still showed 24 CPUs. The session has to be terminated; the next wslc command starts a new one with the new settings:
wslc --session wslc-cli-spare system session terminate(wslc system session terminate wslc-cli-spare, with the name as an argument, fails: Found a positional argument when none was expected.)
4 GB is too tight for the rest of the course (qdrant). Setting kept on this machine, given the memory incident of 2026-09-12: cpuCount: 8, memorySize: 16GB → nproc=8, RAM 15996 MB, swap 16384 MB.
Verification: admin session and Windows-side memory
Section titled “Verification: admin session and Windows-side memory”The admin session reads the same file. In an administrator terminal, wslc info shows the same Settings file: C:\Users\spare\AppData\Local\wslc\settings.yaml. After terminating the admin session:
> wslc --session wslc-cli-admin-spare system session terminate> wslc run --rm alpine sh -c 'echo nproc=$(nproc); free -m'nproc=8Mem: 15996 ...Swap: 16384 ...On the Windows side, each session VM is a process named vmmem<session> (vmmemwslc-cli-spare, vmmemwslc-cli-admin-spare); hcsdiag list (administrator) shows it as a Running VM named after the session. Its memory figures are only readable from an elevated terminal.
Test: a container that holds 6 GB for two minutes, in the non-elevated session.
wslc run -d --name memtest alpine sh -c 'apk add -q stress-ng && stress-ng --vm 1 --vm-bytes 6G --vm-hang 0 --timeout 120s'| Moment | VM process | Working set | Private memory |
|---|---|---|---|
| idle session (admin, nothing running) | vmmemwslc-cli-admin-spare |
921 MB | 970 MB |
| 6 GB held in the container | vmmemwslc-cli-spare |
7069 MB | 7083 MB |
10 s after container stop + remove |
vmmemwslc-cli-spare |
2776 MB | 7084 MB |
| a moment later | vmmemwslc-cli-spare |
902 MB | 1902 MB |
- An idle session VM costs about 0.9 GB.
- The memory used by the container shows up almost entirely on the Windows side (~6 GB + the base VM).
- After the container stops, the memory is not given back immediately: it decreases progressively.
memorySizeis a ceiling, not a reservation: the VM only takes what it uses.
Between two snapshots, the admin session VM had disappeared (nothing running in it): consistent with idleTimeout (30 s) — measured below.
2026-09-13 — Remaining open questions
Section titled “2026-09-13 — Remaining open questions”When is an idle VM torn down?
Section titled “When is an idle VM torn down?”Run one container, then watch the VM process without running any wslc command (a wslc command wakes the session up):
wslc run --rm alpine true# then, every 5 s:Get-Process -Name 'vmmemwslc-cli-spare' -ErrorAction SilentlyContinue 0s VM running: True 35s VM running: FalseThe VM stops 30 to 35 s after the last command (5 s polling): that’s idleTimeout: 30. The session stays listed by wslc system session list; only the VM is torn down, and the next command starts it again.
Docker Desktop with WSL 2.9.11
Section titled “Docker Desktop with WSL 2.9.11”docker desktop start answered Docker Desktop is already running while no Docker Desktop process existed, and docker desktop status answered Could not retrieve status. Launching C:\Program Files\Docker\Docker\Docker Desktop.exe directly worked: engine 29.2.1 ready after ~130 s, docker run --rm hello-world OK. Docker Desktop 4.61 works with WSL 2.9.11.
Are Docker and wslc images shared?
Section titled “Are Docker and wslc images shared?”No. After docker pull busybox:
> docker images > wslc imagespostgres:16-alpine alpine latestbusybox:latesthello-world:latestnode:18-alpineEach tool has its own store (docker_data.vhdx ≈ 50 GB for Docker, storage.vhdx per session for wslc). An image used by both is downloaded twice.
Can wslc and Docker Desktop publish the same port?
Section titled “Can wslc and Docker Desktop publish the same port?”Test: nginx in wslc, httpd (Apache, “It works!”) in Docker, so the answer tells which one responds.
wslc run -d --name webwslc -p 8080:80 nginxdocker run -d --name webdocker -p 8080:80 httpdcurl.exe http://127.0.0.1:8080/ # nginx → wslccurl.exe http://localhost:8080/ # It works! → DockerBoth start without any error: the conflict is silent. Windows accepts both listeners because they don’t bind exactly the same address:
TCP 0.0.0.0:8080 LISTENING com.docker.backendTCP [::]:8080 LISTENING com.docker.backendTCP 127.0.0.1:8080 LISTENING dllhost ← wslcTCP [::1]:8080 LISTENING wslrelay127.0.0.1 goes to wslc (the more specific address wins), while localhost resolves to ::1 first and ends up on Docker. Same result in every variant tried:
| Variant | Errors | 127.0.0.1 |
localhost |
|---|---|---|---|
wslc first, then Docker (8080) |
none | wslc | Docker |
Docker first, then wslc (8081) |
none | wslc | Docker |
Docker, then wslc -p 0.0.0.0:8082:80 |
none | wslc | Docker |
wslc -p 0.0.0.0:8083:80, then Docker |
none | wslc | Docker |
Lesson: don’t publish the same port from both tools. Nothing warns you, and the answer depends on whether the client uses 127.0.0.1 or localhost.
Does storage.vhdx grow and shrink?
Section titled “Does storage.vhdx grow and shrink?”Size of %LocalAppData%\wslc\sessions\wslc-cli-spare\storage.vhdx:
| Step | File size |
|---|---|
start (alpine, nginx) |
814 MB |
wslc pull mcr.microsoft.com/dotnet/sdk:9.0 (869 MB) |
1070 MB |
wslc image remove of that image |
1070 MB |
wslc image prune --all (“Total reclaimed space: 178.5MB”, nothing left) |
1070 MB |
system session terminate |
1070 MB |
pull dotnet/sdk:9.0 again |
1070 MB |
+ dotnet/aspnet:9.0 (224 MB) + eclipse-temurin:21-jdk (491 MB) |
1550 MB |
- The file grows when images are added, and never shrinks by itself: neither
image remove, norprune, nor terminating the session gives space back to Windows. - Space freed inside is reused: pulling the SDK again didn’t make the file grow.
- The growth is less than the displayed image size (
SIZEis the uncompressed size, and freed space is reused), so the file size is not a reliable image counter.
Trap: in wslc image prune, -f means --filter, not --force; --force doesn’t exist (wslc image prune --all doesn’t ask for confirmation).
Compacting storage.vhdx
Section titled “Compacting storage.vhdx”After the lesson 4 builds, the file had grown to 3995 MB, while df inside the session showed only 1.9 GB used. Steps:
# 1. Stop the session VM (non-elevated) and check it's gonewslc --session wslc-cli-spare system session terminateGet-Process -Name 'vmmemwslc-cli-spare' -ErrorAction SilentlyContinue # nothing
# 2. Administrator PowerShell (Hyper-V module): back up, then compact$f = "$env:LOCALAPPDATA\wslc\sessions\wslc-cli-spare\storage.vhdx"Copy-Item $f "$f.bak"Optimize-VHD -Path $f -Mode Fullbefore: 3,995 MBOptimize-VHD -Mode Full: OK in 10safter: 2,789 MB1.2 GB given back to Windows in 10 seconds. Check before deleting the backup: wslc image list still lists csharp-api, java-reactor-api and alpine, and both APIs still answer after wslc run.
Optimize-VHDneeds an administrator terminal and the Hyper-V PowerShell module (present here).- The session VM must be stopped: otherwise the VHDX is in use.
- The file remains larger than the space used inside (2.8 GB vs 1.9 GB): only fully free blocks are reclaimed.
fstrimisn’t available in the session VM:wslc system session run fstrim -v /→Failed to launch command fstrim. Errno = 2.
2026-09-13 — Lessons 3 and 4 done
Section titled “2026-09-13 — Lessons 3 and 4 done”- Lesson 3 (containers, ports,
exec): practiced throughout the entries above —nginxpublished with-p,exec,container list --all, and the port conflict with Docker Desktop. - Lesson 4 (building an image): rewritten with a C# API and a Spring Boot WebFlux API, built and run with
wslc; every output in the lesson is real.
2026-09-13 — Running qdrant with wslc
Section titled “2026-09-13 — Running qdrant with wslc”Before starting: a qdrant is already running in Docker
Section titled “Before starting: a qdrant is already running in Docker”> docker psga-qdrant qdrant/qdrant:latest Up About an hour (healthy) 0.0.0.0:6333-6334->6333-6334/tcp, [::]:6333-6334->6333-6334/tcpPublishing wslc on 6333 too would start without any error and silently take over 127.0.0.1:6333 from the application that uses ga-qdrant (see the port conflict above). So the wslc qdrant is published on 16333/16334.
Pull: same tag, different version
Section titled “Pull: same tag, different version”wslc pull qdrant/qdrant # 21 s, 198 MBThe Docker image was already there, but wslc downloads it again (separate stores). And latest isn’t the same version on both sides:
> curl.exe http://127.0.0.1:16333/ # wslc{"title":"qdrant - vector search engine","version":"1.19.1","commit":"6ab21cac18ebb6f4ae29102c7f8f5cc11affd5de"}> curl.exe http://127.0.0.1:6333/ # Docker (ga-qdrant, pulled on 2025-12-19){"title":"qdrant - vector search engine","version":"1.16.3","commit":"bd49f45a8a2d4e4774cac50fa29507c4e8375af2"}Lesson: latest means “whatever was latest when this tool pulled it”. Pin a version (qdrant/qdrant:v1.19.1) when two environments must match.
Run with a volume
Section titled “Run with a volume”wslc volume create qdrant-datawslc run -d --name qdrant -p 16333:6333 -p 16334:6334 -v qdrant-data:/qdrant/storage qdrant/qdrantcurl.exe http://127.0.0.1:16333/readyz # all shards are ready (after 1 s)CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESda52872e3246 qdrant/qdrant "./entrypoint.sh" 5 seconds ago Up 1 second 127.0.0.1:16333->6333/tcp, 127.0.0.1:16334->6334/tcp qdrantTrap: the log says Access web UI at http://localhost:6333/dashboard. That’s the port inside the container. From Windows, the dashboard is at http://127.0.0.1:16333/dashboard — and localhost:6333 would open the Docker one.
A collection, three points and a query (bodies in JSON files, to avoid PowerShell quoting issues):
curl.exe -X PUT http://127.0.0.1:16333/collections/journal -H "Content-Type: application/json" --data-binary "@coll.json"curl.exe -X PUT "http://127.0.0.1:16333/collections/journal/points?wait=true" -H "Content-Type: application/json" --data-binary "@points.json"curl.exe -X POST http://127.0.0.1:16333/collections/journal/points/query -H "Content-Type: application/json" --data-binary "@query.json"coll.json {"vectors":{"size":4,"distance":"Cosine"}}points.json {"points":[{"id":1,"vector":[0.9,0.1,0.1,0.1],"payload":{"note":"wslc sessions"}},{"id":2,"vector":[0.1,0.9,0.1,0.1],"payload":{"note":"ports and localhost"}},{"id":3,"vector":[0.1,0.1,0.9,0.1],"payload":{"note":"storage.vhdx"}}]}query.json {"query":[0.2,0.8,0.1,0.1],"limit":2,"with_payload":true}{"result":true,"status":"ok","time":0.24333272}{"result":{"operation_id":1,"status":"completed"},"status":"ok","time":0.003567116}{"result":{"points":[{"id":2,"version":1,"score":0.99111706,"payload":{"note":"ports and localhost"}},{"id":1,"version":1,"score":0.3651484,"payload":{"note":"wslc sessions"}}]},"status":"ok","time":0.00189502}Persistence: delete the container, keep the data
Section titled “Persistence: delete the container, keep the data”wslc container stop qdrantwslc container remove qdrantwslc run -d --name qdrant -p 16333:6333 -p 16334:6334 -v qdrant-data:/qdrant/storage qdrant/qdrantcurl.exe http://127.0.0.1:16333/collectionscurl.exe -X POST http://127.0.0.1:16333/collections/journal/points/count -H "Content-Type: application/json" -d "{}"{"result":{"collections":[{"name":"journal"}]},"status":"ok","time":8.866e-6}{"result":{"count":3},"status":"ok","time":0.007211133}The collection and its 3 points survive, because they live in the volume. wslc volume inspect qdrant-data shows "Driver": "guest" and a mountpoint inside the session VM (/var/lib/docker/volumes/qdrant-data/_data), so in the session’s storage.vhdx.
Don’t mount a Windows folder for qdrant storage
Section titled “Don’t mount a Windows folder for qdrant storage”wslc run -d --name qdrant-bind -p 16335:6333 -v "C:\...\qdrant-bind:/qdrant/storage" qdrant/qdrantwslc exec qdrant-bind sh -c "mount | grep /qdrant/storage"drvfs on /qdrant/storage type virtiofs (rw,relatime)qdrant still starts and writes its files into the Windows folder, but logs an error:
ERROR qdrant: Filesystem check failed for storage path ./storage. Details: FUSE filesystems may cause data corruption due to caching issuesLesson: for a database, use a wslc volume (inside the VM), not a bind mount of a Windows folder.
Resources
Section titled “Resources”> wslc stats qdrantCONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDSda52872e3246 qdrant 0.16% 47.21MiB / 15.62GiB 0.30% 10.1kB / 5.77kB 8.19kB / 184kB 35> docker stats ga-qdrant --no-stream --format "CPU {{.CPUPerc}} MEM {{.MemUsage}}"CPU 0.41% MEM 367.8MiB / 31.2GiB- The limit shown is the VM’s: 15.62 GiB for
wslc(thememorySize: 16GBsetting), 31.2 GiB for Docker Desktop (half of the RAM by default). - 47 MiB for 3 points against 368 MiB for
ga-qdrant: not comparable,ga-qdrantholds real data. - On the Windows side, the
vmmemwslc-cli-spareVM was at 1160 MB (about 0.9 GB idle, measured earlier). - qdrant logs
starting 7 workers: it sees the 8 CPUs allowed bycpuCount: 8.
Cleanup: wslc container stop qdrant qdrant-bind, wslc container remove qdrant qdrant-bind, wslc volume remove qdrant-data. ga-qdrant was never touched.
2026-09-13 — Compose with wslc
Section titled “2026-09-13 — Compose with wslc”No compose command
Section titled “No compose command”> wslc compose --helpUnrecognized command: 'compose'wslc --help lists no Compose equivalent, and the official tutorial doesn’t mention it. wslc 2.9.11 has no Compose support.
Dead end: plugging docker compose into the session engine
Section titled “Dead end: plugging docker compose into the session engine”The session VM does run a Docker engine:
> wslc system session run ps -eo pid,args 131 /usr/bin/containerd --address /run/containerd/containerd.sock --root /var/lib/docker/containerd/daemon --state /run/docker/containerd/daemon 132 /usr/bin/dockerd --containerd /run/containerd/containerd.sock> wslc system session run ls -la /var/run/docker.socksrw-rw---- 1 root docker 0 Sep 13 18:47 /var/run/docker.sockBut nothing exposes it to Windows (no wslc named pipe), the VM’s own docker client fails (wslc system session run docker version → The handle is invalid. Error code: ERROR_INVALID_HANDLE), and it can’t be mounted into a docker:cli container (which includes Compose v5.5.1):
wslc run --rm -e DOCKER_HOST=unix:///var/run/docker.sock -v /var/run/docker.sock:/var/run/docker.sock docker:cli docker psCannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?Why: the source of -v is a Windows path. /var/run/docker.sock became C:\var\run\docker.sock, mounted over virtiofs:
drvfs on /run/docker.sock type virtiofs (rw,relatime)Trap: wslc creates a missing bind-mount source. This test left an empty C:\var\run\docker.sock\ folder on Windows (removed afterwards). --mount type=bind,source=/var/run/docker.sock,... is refused: The bind source path must be absolute.
What Compose really provides, done by hand
Section titled “What Compose really provides, done by hand”docker compose config on a two-service file shows what Compose adds implicitly: one network per project, named volumes, and service names as DNS names. On a network created with wslc network create, names do resolve:
> wslc run --rm --network demo alpine wget -qO- http://qdrant:6333/{"title":"qdrant - vector search engine","version":"1.19.1","commit":"6ab21cac18ebb6f4ae29102c7f8f5cc11affd5de"}> wslc run --rm --network demo alpine wget -qO- http://vectors:6333/readyz # --network-alias vectorsall shards are ready> wslc run --rm alpine wget -qO- -T 5 http://qdrant:6333/ # default networkwget: bad address 'qdrant:6333'The compose.yaml (validated with docker compose -p wslcdemo config):
services: qdrant: image: qdrant/qdrant:v1.19.1 ports: - "127.0.0.1:16333:6333" volumes: - qdrant-data:/qdrant/storage
seed: image: curlimages/curl:8.16.0 depends_on: - qdrant command: > --silent --show-error --retry 10 --retry-connrefused --retry-delay 1 -X PUT http://qdrant:6333/collections/demo -H "Content-Type: application/json" -d '{"vectors":{"size":4,"distance":"Cosine"}}'
volumes: qdrant-data:Its wslc translation, wslc-up.ps1:
# Equivalent of `docker compose -p wslcdemo up -d` for compose.yaml, with wslc$project = 'wslcdemo'
# What Compose creates implicitly: one network per project, named volumeswslc network create "${project}_default"wslc volume create "${project}_qdrant-data"
# service qdrant (the service name becomes a DNS alias on the project network)wslc run -d --name "$project-qdrant-1" --network "${project}_default" --network-alias qdrant ` -p 127.0.0.1:16333:6333 -v "${project}_qdrant-data:/qdrant/storage" qdrant/qdrant:v1.19.1
# service seed (depends_on only orders the start: curl retries until qdrant answers)wslc run --name "$project-seed-1" --network "${project}_default" --network-alias seed ` curlimages/curl:8.16.0 --silent --show-error --retry 10 --retry-connrefused --retry-delay 1 ` -X PUT http://qdrant:6333/collections/demo -H 'Content-Type: application/json' ` -d '{"vectors":{"size":4,"distance":"Cosine"}}'And wslc-down.ps1:
# Equivalent of `docker compose -p wslcdemo down --volumes`$project = 'wslcdemo'wslc container stop "$project-qdrant-1"wslc container remove "$project-qdrant-1" "$project-seed-1"wslc network remove "${project}_default"wslc volume remove "${project}_qdrant-data"Result of wslc-up.ps1:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES28be34e431f5 curlimages/curl:8.1… "/entrypoint.sh --si…" 1 second ago Exited (0) Less than a second ago wslcdemo-seed-1c1f82d52d6ca qdrant/qdrant:v1.19… "./entrypoint.sh" 2 seconds ago Up 1 second 127.0.0.1:16333->6333/tcp wslcdemo-qdrant-1> wslc logs wslcdemo-seed-1{"result":true,"status":"ok","time":0.409039704}> curl.exe http://127.0.0.1:16333/collections{"result":{"collections":[{"name":"demo"}]},"status":"ok","time":0.00004439}After wslc-down.ps1: no container, only the default bridge/host/none networks, no volume.
Conclusion: for a small stack, a wslc script reproduces what Compose does (network, volumes, DNS names, start order). What it doesn’t give: no reading of compose.yaml, no depends_on with condition: service_healthy, no diff-based up that only recreates what changed, no logs -f over all services. For real Compose projects, Docker Desktop (or Podman) remains the tool.
2026-09-13 — A C# program with Microsoft.WSL.Containers
Section titled “2026-09-13 — A C# program with Microsoft.WSL.Containers”Goal: drive a container from a Windows application, without wslc.exe. The code is in code/wsl-containers/wslc-host; lesson 5 shows it.
The documented snippet doesn’t compile
Section titled “The documented snippet doesn’t compile”Pasting the Microsoft Learn snippet into a project that references Microsoft.WSL.Containers 2.9.9 gives five errors:
error CS0103: The name 'ComponentFlags' does not exist in the current contexterror CS0117: 'SessionSettings' does not contain a definition for 'MemoryMB'error CS0117: 'ProcessSettings' does not contain a definition for 'CmdLine'error CS0103: The name 'DeleteContainerFlags' does not exist in the current contexterror CS1705: Assembly 'wslcsdkcs' with identity 'wslcsdkcs, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' uses 'Microsoft.Windows.SDK.NET, Version=10.0.26100.79, Culture=neutral, PublicKeyToken=31bf3856ad364e35' which has a higher version than referenced assembly 'Microsoft.Windows.SDK.NET' with identity 'Microsoft.Windows.SDK.NET, Version=10.0.19041.38, Culture=neutral, PublicKeyToken=31bf3856ad364e35'The real names come from reading the public types of wslcsdkcs.dll with MetadataLoadContext:
| Microsoft Learn | Package 2.9.9 |
|---|---|
ComponentFlags GetMissingComponents() |
IReadOnlyList<Component> GetMissingComponents() (VirtualMachinePlatform, WslPackage, SdkNeedsUpdate) |
SessionSettings.MemoryMB |
SessionSettings.MemorySizeInMB |
ProcessSettings.CmdLine |
ProcessSettings.CommandLine (IList<string>) |
DeleteContainerFlags.None |
DeleteContainerOption.None / Force |
CS1705: the Windows SDK version
Section titled “CS1705: the Windows SDK version”The package declares net8.0-windows10.0.19041.0, but its wslcsdkcs.dll is compiled against Microsoft.Windows.SDK.NET 10.0.26100.79. Moving to net10.0-windows10.0.26100.0 isn’t enough (the SDK picks projection 10.0.26100.38, same error); the version has to be forced. 10.0.26100.79 doesn’t exist on nuget.org (NU1102, nearest: 10.0.26100.80):
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework><WindowsSdkPackageVersion>10.0.26100.80</WindowsSdkPackageVersion><RuntimeIdentifier>win-x64</RuntimeIdentifier>First run
Section titled “First run”WSL container service 2.9.11Session started, storage in C:\Users\spare\AppData\Local\WslcHostImage alpine:latest (8 MB)Hello from 3.24.1 on 6.18.40.1-microsoft-standard-WSL2Container b27dd2f80927 exited with code 07 s in total, including the alpine pull into a new, empty session. The container sees the session’s limits: nproc → 2, free -m → 1907 MB total for MemorySizeInMB = 2048.
Where the application’s session shows up
Section titled “Where the application’s session shows up”While a 20-second container runs:
> wslc system session listID Creator PID Display Name6 350476 wslc-cli-admin-spare8 275812 wslc-cli-spare16 110952 wslc-host> Get-Process vmmem*vmmemCmZygote 0vmmemWSL 3307vmmemwslc-host 510> wslc --session wslc-host container listCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES2e75803c92a9 alpine:latest "/bin/sh -c 'echo st…" 4 seconds ago Up 3 seconds wslc-host-hello- The session is a regular
wslcsession: samevmmem<session>VM, visible and drivable from the CLI with--session. - Its disk is where the application chose,
%LocalAppData%\WslcHost\storage.vhdx(67 MB after the pull), not under%LocalAppData%\wslc\sessions. Its limits come fromSessionSettings, not fromsettings.yaml(8 CPUs there, 2 seen by the container). - After
session.Terminate(), the session and itsvmmemdisappear right away, without the CLI’s 30 s idle delay.
Trap: a failed Start leaves the container behind
Section titled “Trap: a failed Start leaves the container behind”A run launched from Git Bash with /bin/sh as an argument: MSYS converts the path to C:/Program Files/Git/usr/bin/sh, and container.Start() throws:
Unhandled exception. System.ArgumentException: The parameter is incorrect.
failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "C:/Program Files/Git/usr/bin/sh": stat C:/Program Files/Git/usr/bin/sh: no such file or directory: unknownThe next run fails on CreateContainer:
Unhandled exception. System.Runtime.InteropServices.COMException (0x800700B7): Cannot create a file when that file already exists.
Conflict. The container name "/wslc-host-hello" is already in use by container "176a59b772bebfb974a8404150407157d49cc279cbe2f66c2d6e2788f9bfbadc". You have to remove (or rename) that container to be able to reuse that name.The container survives the process in storage.vhdx. Two fixes in the program: Delete(DeleteContainerOption.Force) in a finally, and on startup OpenContainer + Delete of a leftover (COMException if there is none). Verified: leftover removed (Removed leftover container wslc-host-hello), then a command that doesn’t exist (/nope) no longer leaves anything behind.
Conclusion: the API works and is fast, but in preview its documentation lags behind the package: names, SDK version. Compile first, read the types if in doubt. On GitHub Actions the Windows runners have no WSL containers service: CI only compiles the program.
Open questions
Section titled “Open questions”Where doesInwslcstore its images and containers?%LocalAppData%\wslc\sessions\<session>\storage.vhdx, one virtual disk per session. It grows with images and doesn’t shrink on its own (see above).Can the memory and CPU of the VM used byYes:wslcbe limited?cpuCountandmemorySizeinsettings.yaml, then terminate the session (see above).CanThey can publish the same port without any error, which is the problem:wslcand Docker Desktop publish ports without conflict?127.0.0.1reacheswslc,localhostreaches Docker (see above).How do you compact a session’sTerminate the session, thenstorage.vhdx?Optimize-VHD -Mode Fullas administrator: 3995 MB → 2789 MB (see above).- Does the MSBuild
WslcImageintegration (building an image to a.tarduringdotnet build) work? To verify.