wslc lets you run Linux containers on Windows 11 straight from PowerShell, with no Docker Desktop and no distro to manage. Most of its commands read like Docker commands, so this wslc cheat sheet focuses on the places where they don’t: compose, prune, flags that are missing and a config file that isn’t called config.
You get every wslc command grouped by task, a wslc vs docker mapping table, the honest wslc compose status and the real output of each subcommand. We checked every command in November 2026 on a Windows 11 24H2 lab VM with WSL and wslc 3.0.3, and we read each flag from wslc <command> --help on that box rather than from Docker habits.
wslc cheat sheet: commands by task
Every row below ran on our lab VM as a normal admin user, using the names from this page (web, app, db, appnet, pgdata). Most ran through a cmd.exe capture script, and we repeated the core run, ps, exec, network and prune flow in Windows PowerShell, where only the quoting of exec one-liners behaved differently. Commands that ask for confirmation are shown with -f, because a prune without it waits for a y/N answer and hangs any script that runs it. The tables are also the content of the PDF, so what you print is what we tested.
Version, info and sessions
| Command | What it does |
|---|---|
wsl --version | WSL, kernel and Windows build numbers |
wslc version | Print the wslc CLI version |
wslc info | Client versions, settings file path and running sessions |
wslc system session list | List session VMs for your user |
wslc system session terminate | Stop the session VM; the next command boots a fresh one |
wslc --help | List every top-level command |
Images and builds
| Command | What it does |
|---|---|
wslc pull nginx:latest | Pull an image from Docker Hub |
wslc images | List images (same as wslc image ls) |
wslc images --digests | List images with their sha256 digests |
wslc build -t hello:1.0 . | Build the Dockerfile in the current folder and tag it |
wslc tag nginx:latest web:test | Add a second tag to an image |
wslc rmi web:test | Remove an image or one of its tags |
Run containers
| Command | What it does |
|---|---|
wslc run --rm alpine:latest cat /etc/os-release | One-off container, removed when it exits |
wslc run -d --name web -p 8080:80 nginx:latest | Background container on 127.0.0.1:8080 |
wslc run -d --name web -p 0.0.0.0:8080:80 nginx:latest | Publish on every host interface |
wslc run -d --name db --network appnet -e POSTGRES_PASSWORD=labpass -v pgdata:/var/lib/postgresql postgres:18 | User network, env variable and named volume |
wslc start web | Start a stopped container |
wslc restart web | Restart a container |
List, logs, exec and stats
| Command | What it does |
|---|---|
wslc ps | Running containers (aliases: wslc list, wslc ls) |
wslc ps -a | All containers, including stopped ones |
wslc ps --format json | One JSON object per container |
wslc logs web | Print the logs of a container |
wslc logs --tail 2 -t web | Last 2 lines with timestamps |
wslc exec web nginx -v | Run a command in a running container |
wslc exec -w /usr/share/nginx/html web ls -l | Run a command in a chosen working directory |
wslc stats web | One snapshot of CPU, memory, network and block I/O |
wslc inspect --format json db | Container details as single-line JSON |
wslc container cp web:/etc/nginx/nginx.conf . | Copy a file out (there is no top-level wslc cp) |
Stop and remove
| Command | What it does |
|---|---|
wslc stop web | Send the image STOPSIGNAL (SIGTERM if none), then kill after the timeout |
wslc stop -t 2 web | Wait only 2 seconds before killing |
wslc rm web | Remove a stopped container (aliases: remove, delete) |
wslc rm -f app db | Stop and remove running containers in one step |
wslc rm -v web | Also remove the anonymous volumes of the container |
Networks and volumes
| Command | What it does |
|---|---|
wslc network create appnet | User network with name-based DNS between containers |
wslc network ls | List networks (bridge, host and none exist by default) |
wslc network connect appnet web | Attach a running container to a network |
wslc network rm appnet | Delete a network |
wslc volume create pgdata | Named volume inside the session disk |
wslc volume ls | List volumes and their driver |
wslc volume rm pgdata | Delete a volume and its data |
Prune (cleanup)
| Command | What it does |
|---|---|
wslc container prune -f | Delete all stopped containers without a prompt |
wslc image prune -f | Delete dangling images only |
wslc image prune -a -f | Delete every image no container uses |
wslc volume prune -f | Delete unused anonymous volumes only |
wslc volume prune -a -f | Also delete unused named volumes |
wslc network prune -f | Delete networks no container uses |
Settings (the wslc config file)
| Command | What it does |
|---|---|
wslc settings | Create settings.yaml on first run and open it in the default editor |
wslc settings reset | Overwrite settings.yaml with the commented defaults |
Get-Content $env:LOCALAPPDATA\wslc\settings.yaml | Print the settings file in PowerShell |
Two rows trip up nearly everyone coming from Docker. The publish row binds to 127.0.0.1 unless you add 0.0.0.0:, and the volume prune row leaves named volumes alone unless you add -a. Both are covered with real output further down, along with the timings that make the first command of a session feel broken when it isn’t.
How do wslc commands map to docker?
For day-to-day container work, swap docker for wslc and most commands run unchanged, because wslc drives a standard container engine inside its session VM. The differences sit in the client, and they are the ones that break scripts copied from a Linux box. This table lists what changed when we replayed Docker habits against wslc.
| Docker command | wslc equivalent | What is different |
|---|---|---|
docker ps | wslc ps | Also wslc list and wslc ls; same -a, -q and –format json flags |
docker run -p 8080:80 | wslc run -p 8080:80 | Binds 127.0.0.1 unless you write 0.0.0.0:8080:80 |
docker compose up -d | None | Unrecognized command: ‘compose’ |
docker system prune | None | Run the container, image, volume and network prunes one by one |
docker cp | wslc container cp | No top-level cp in this release |
docker inspect -f '{{.State.Status}}' db | wslc inspect --format json db | A Go template fails: Supported format types are: json |
docker run --platform linux/arm64 | None | Option name was not recognized |
docker stats --no-stream | wslc stats | wslc stats prints one snapshot and exits |
docker logs -f web | wslc logs -f web | Same -f, -n, -t, –since and –until flags |
docker version | wslc version | No docker.exe shim is installed |
The missing pieces decide whether wslc can replace Docker for you, so check your own workflow against this list before you uninstall anything.
- Your scripts call only run, ps, logs, exec, build, pull and the network and volume verbs.
- No tool in your build talks to a Docker socket, because wslc exposes no docker.exe and no DOCKER_HOST.
- You can live without a compose.yaml for now, or your stack is two or three services.
- Every image you run publishes an amd64 build, since –platform is rejected.
If all four hold, wslc covers your day and you can drop the Docker Desktop licence question entirely. If one fails, keep Docker Engine in a WSL distro alongside it, and keep our Docker CLI and Compose cheat sheet open for that side. Teams that prefer daemonless containers can compare the Podman commands too, since Podman also runs on Windows through WSL. If speed decides it, our wslc vs Docker benchmark timed cold start, memory, file I/O, builds and PostgreSQL start-up on the same Windows 11 machine. For the concepts behind images, layers and networks, which carry over to wslc unchanged, the Nigel Poulton book Docker Deep Dive (paid link) (2025 edition, Kindle) is the one we hand to people new to containers.
Does wslc compose work yet?
No. wslc 3.0.3 has no compose command, so wslc compose up fails at the parser before anything reaches the session VM. We tried it from a folder holding a one-service compose.yaml.
wslc compose up -d
The answer is the same message any unknown verb gets, followed by the full command list:
Unrecognized command: 'compose'
Usage: wslc [global-options] [<command>] [options]
Plain docker compose fails too, because the WSL package installs no docker.exe. The capture below, taken in Command Prompt, shows the compose.yaml, both failures and the command list that wslc prints instead.

Compose work is happening upstream. The WSL repository has a feature/compose branch, and Microsoft merged the latest master into it on October 16, 2026 (PR 41893). No stable or preview release ships it yet, so until one does, a small stack is a handful of wslc commands in the order compose would run them.
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
wslc run -d --name app --network appnet alpine:latest sleep 3600
Service discovery is the part people worry about, and it works on a user network. From inside app, the name db resolves and answers a ping:
wslc exec app getent hosts db
The address belongs to the postgres container on appnet.
172.18.0.2 db db
Test with getent hosts, not nslookup. The busybox nslookup in Alpine appended the DNS search suffixes the container inherited from our LAN and returned NXDOMAIN for every one of them, which looks like broken DNS when it isn’t. The network matters too: two alpine containers started without --network landed on the default bridge, and getent hosts between them found nothing and exited with code 2. The full exchange on appnet, with the ping and both containers up, looked like this.

What you give up without compose is everything that lives in the YAML. Before you script a stack by hand, check that these three gaps are acceptable for your project:
- Your app retries its database connection, because there is no depends_on and the postgres:18 pull plus first start took 125.5 seconds on our VM.
- No two checkouts on the machine create the same appnet and db names, since nothing prefixes names per project and they collide in one session.
- Someone owns the PowerShell file that replaces up and down, so it does not drift from the compose.yaml your team keeps for Docker.
For two or three services we would script it that way. For more, stay on Docker Compose until wslc ships its own, because a hand-written start order breaks the first time a service is added.
How do you run and inspect a container with wslc?
Use wslc run -d with a name and a published port, then check it with wslc ps. Expect the first command of a new session to be slow, because wslc creates and formats the session VM before it pulls a single layer. Our first wslc pull nginx:latest took 208.6 seconds; the same 163MB image, pulled by wslc run on an already running session, later took 83.3 seconds, so roughly two minutes of that first wait was the session start. Once the image was local, wslc run -d returned in 2.4 seconds. The session also shuts down after 30 seconds idle by default, so a gap between test runs brings part of that cold start back.
wslc run -d --name web -p 8080:80 nginx:latest
List it and read the PORTS column, which is where wslc parts ways with Docker.
wslc ps
The port is bound to loopback only, so the page answers on localhost but not from another machine on your network:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9edce8831513 nginx:latest "/docker-entrypoint.…" 25 seconds ago Up 9 seconds 127.0.0.1:8080->80/tcp web
We would keep that default on a laptop, because a database published with -p 5432:5432 stays off the coffee-shop Wi-Fi. If you do need the container reachable from the LAN, write -p 0.0.0.0:8080:80; our run with that flag showed 0.0.0.0:8080->80/tcp in the same column. Logs, exec and stats then behave the way Docker users expect.
wslc logs --tail 2 -t web
Each line carries an RFC3339 timestamp from the container runtime, and the first one is our own curl request.
2026-11-01T05:19:18.857832000Z 169.254.73.254 - - [01/Nov/2026:05:19:18 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.21.0" "-"
2026-11-01T05:19:18.285482300Z 2026/11/01 05:19:18 [notice] 1#1: start worker process 32
Look at the client address: 169.254.73.254, not 127.0.0.1. Requests from Windows reach the container through the network relay of the session, so access logs and IP allowlists inside the container see that link-local address instead of loopback. If an app only trusts 127.0.0.1, it will reject your browser until you allow that range. To run commands inside the container, pass its name to wslc exec.
wslc exec -e GREETING=karibu web sh -c 'echo $GREETING from $(hostname)'
The variable and the container hostname both come back from inside the container.
karibu from 9edce8831513
Use single quotes in PowerShell. With double quotes, PowerShell expands $GREETING and $(hostname) on the Windows side before wslc sees them, so the same command printed from win11-lab: an empty variable and the name of the Windows machine instead of the container. Command Prompt passes the dollar signs through untouched, which is why Linux one-liners copied into cmd.exe work with double quotes and break when you paste them into PowerShell.
The window below holds the whole session, including wslc stats web. That command printed one snapshot and returned, so you don’t need the --no-stream flag from Docker in scripts. The memory limit of 5.778GiB is what the session VM got from the 12 GB we gave the lab VM, which matches the documented default of half the memory Windows reports.

For scripts, wslc ps --format json prints one JSON object per container with State, Ports and Networks fields. When we attached web to appnet with wslc network connect, its Networks field read appnet,bridge, which is a quick way to confirm a container joined the network you meant. wslc inspect --format json gives the full record, but --format takes only json: a Docker-style --format '{{.State.Status}}' failed with “Invalid format value”. Pipe it into ConvertFrom-Json in PowerShell instead of reaching for a Go template.
How do you remove containers and prune with wslc?
Use wslc rm -f for running containers and the four separate prune commands for cleanup, always with -f in scripts. A plain wslc rm on a running container refuses with a named error code.
wslc rm web
The error tells you both ways out, stop first or force it:
Container '9edce8831513b6683993a7f8e5e2ad0338ca59eeafe5daa0801ea16fd5d69b36' is running and cannot be removed. Either stop the container before removing or use forced remove (-f).
Error code: WSLC_E_CONTAINER_IS_RUNNING
Forcing it with wslc rm -f web took 4 to 6 seconds across our runs. A plain wslc stop can take longer than you expect, because it sends the stop signal of the image (SIGTERM unless the image sets another) and waits for the process to exit. Our alpine container running sleep 3600 ignored the signal, so wslc stop app sat out the timeout, took 16 seconds and ended with exit code 137. Pass -t 2 when you do not care about a clean shutdown, and leave it off for databases.

Pruning is where wslc and Docker differ the most. There’s no wslc prune and no wslc system prune, and both return “Unrecognized command: ‘prune'”. Instead you get one prune per object type, and each of them asks for confirmation unless you pass -f.
wslc container prune
Run from a scheduled task with no console, the prompt below sat for 81 seconds until we killed the process. A CI job or a login script would hang the same way, and nothing in the output tells you why.
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N]
The volume prune is the one that surprises people. With the db container gone, wslc volume prune -f reclaimed 0B and left pgdata in place, because without -a it only removes anonymous volumes. Adding -a deleted pgdata and reclaimed 40.72MB, which was the whole PostgreSQL data directory. A second run in PowerShell, below, shows the other half: -f alone removed the anonymous volume left by an earlier postgres container that ran without a named volume and was removed without rm -v, and it still kept pgdata. That split is the safer design, but a cleanup script written for the old Docker behaviour will quietly keep every named volume.

The same split applies to images. wslc image prune -f found nothing dangling and reclaimed 0B, while wslc image prune -a -f untagged alpine, hello, postgres and nginx and freed 541.1MB. Run the four prunes in this order when you clean up a session.
- Run
wslc container prune -fand check thatwslc ps -alists only containers you want. - Run
wslc network prune -fand check thatwslc network lsshows bridge, host and none. - Run
wslc image prune -a -fand check thatwslc imagesis empty or holds only images in use. - Run
wslc volume prune -a -flast, and only after copying out any data you need.
Volumes go last because they’re the only step you can’t undo with a pull. A guest volume lives inside the storage.vhdx of the session, not as files in Explorer, so copy data out with wslc container cp before you prune. In our run that copy took 1.2 seconds for nginx.conf, so there is no reason to skip it.
Where is the wslc config file?
There is no wslc config command; it returns “Unrecognized command: ‘config'”. Configuration lives in %LOCALAPPDATA%\wslc\settings.yaml, and wslc settings manages it. On our VM the file did not exist until the first wslc settings run, which created it at 1,427 bytes and opened it in Notepad. Microsoft documents that it uses your default editor for .yaml files and falls back to Notepad when none is associated, which is what a clean Windows install has.
wslc settings
Every key ships commented out at its default. These are the session keys from the generated file, with the defaults the file itself states.
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: default
# Maximum disk image size (e.g. 500GB default: 1TB)
# maxStorageSize: default
# Default host address that published ports bind to when 'container run -p' is
# used without an explicit address (default: 127.0.0.1)
# defaultBindingAddress: default
# DNS name that resolves to the host loopback address (default: host.wslc.internal).
# Set to "none" to disable the entry.
# hostLoopback: default
# Seconds an idle session VM stays running before it is torn down (default: 30)
# idleTimeout: default
The file also has a storagePath key for moving the session disk off C:, and a top-level credentialStore that defaults to wincred. These are the three we would look at first.
- idleTimeout is raised above 30 seconds if you hit cold starts between test runs.
- memorySize is set explicitly on a 16 GB laptop, so the session cannot take 8 GB from your IDE.
- defaultBindingAddress stays at its default unless every container you run should face the LAN.
The session keys are read when a session starts, so restart it after an edit; defaultBindingAddress and credentialStore are read on every wslc command, according to the WSL container settings reference. wslc info confirms which file the client reads, Get-Content prints it, and wslc system session terminate stops the VM so the next command boots it with the new values.

One thing went wrong here. After the terminate, our capture script started wslc settings in the background, which returned after 28.4 seconds, and a few seconds later ran wslc network create appnet. That network create failed after another 28.4 seconds with “Unspecified error” and code E_FAIL, and the next two commands reported that appnet did not exist. The most likely cause is two wslc processes bringing up the new session at once, although we could not prove it. In the same session, a later wslc network create appnet succeeded in 13.3 seconds. If a command fails like that just after a terminate, run wslc system session list, wait for the session to show up, and retry. According to the settings reference, a bad value in settings.yaml does not stop the session either; wslc prints a warning with the file path and line number and falls back to the default (we did not test that case), and wslc settings reset restores the commented template.
How do you install wslc on Windows 11?
wslc ships inside the WSL package, so installing or updating WSL gives you the CLI. On a machine that already runs WSL from the Microsoft Store, wsl --update is the documented route. Our VM was a clean Windows 11 install, so we enabled the two Windows features from an elevated PowerShell prompt first.
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
Then we downloaded the 367 MB MSI from the latest stable GitHub release with curl.exe and installed it silently. The API lookup returned 3.0.3 on our VM, the install finished in about a minute and a half, and the reboot activates the two features.
$tag = (Invoke-RestMethod https://api.github.com/repos/microsoft/WSL/releases/latest).tag_name
curl.exe -sSL -o $env:TEMP\wsl.msi "https://github.com/microsoft/WSL/releases/download/$tag/wsl.$tag.0.x64.msi"
Start-Process msiexec.exe -ArgumentList "/i $env:TEMP\wsl.msi /qn /norestart" -Wait
Restart-Computer
After the reboot, both version commands should report the same release, here in Command Prompt. A VM needs nested virtualization (CPU type host on Proxmox) for the session VM to start.

You do not need a WSL distro for any of this. wslc runs its own session VM, separate from Ubuntu or the RHEL and Fedora images for WSL you may already use. Our hands-on WSL containers guide goes further into how the session VM is built, Dockerfile builds with health checks, virtiofs mounts and a file I/O benchmark against Docker. The official reference is the WSL containers documentation from Microsoft.
Is your wslc setup ready? A quick checklist
Work through these on your own machine before you move a project over. Each one is a check you can run in under a minute.
wsl --versionandwslc versionreport the same release.wslc system session listshows a session after your firstwslc pull.wslc psshows the port bound to the address you intended, 127.0.0.1 or 0.0.0.0.wslc exec app getent hosts dbresolves containers on your user network.- Every prune in your scripts carries
-f, and volume prunes carry-aonly where you mean it. wslc infopoints at the settings.yaml you edited.
If item 4 fails, check that both containers were started with --network appnet, since the default bridge gave us no name resolution at all. If anything in your workflow still needs compose, keep that project on Docker for now and recheck wslc --help after each WSL release.