wslc CI/CD: run containers in CI without Docker Desktop
wslc runs on GitHub Actions windows-2025 runners with no Docker Desktop license, no daemon, and no Docker Engine. The runner's WSL is upgraded to a pre-release build that includes wslc, and containers work immediately.
Minimal workflow
name: wslc CI
on: [push, workflow_dispatch]
jobs:
containers:
runs-on: windows-2025
steps:
- name: Install wslc
shell: pwsh
run: wsl --update --pre-release
- name: Run tests in a container
shell: pwsh
env:
WSL_UTF8: '1'
run: |
$wslc = 'C:\Program Files\WSL\wslc.exe'
& $wslc version
& $wslc run --rm alpine echo "wslc works on CI" Key differences from Docker-based CI
| Aspect | wslc | Docker |
|---|---|---|
| Install time | ~30 s (wsl --update) | ~2 min (Docker Desktop install) |
| Licence | Free (built into Windows) | Paid on Windows Server |
| Daemon | Daemonless | dockerd background process |
| PATH | Absolute path required | Auto-added to PATH |
| Compose | Not available | Built-in |
| Runner OS | Windows Server 2025 only | Windows Server 2022+ |
Building and pushing images from CI
wslc build, tag, login and push all
work on the runner. For Docker Hub, export DOCKER_USER and
DOCKER_PASS as repository secrets and then:
& $wslc build -t myorg/myapp:latest .
& $wslc login -u $env:DOCKER_USER -p $env:DOCKER_PASS
& $wslc push myorg/myapp:latest Verified images (image-matrix CI)
All ten images below were tested on windows-2025 with wslc 2.9.4.0, all passed:
alpine:3.20debian:bookworm-slimubuntu:24.04python:3.12-slimnode:22-alpinenginx:alpineredis:7-alpinepostgres:16-alpinebusybox:latestmcr.microsoft.com/cbl-mariner/base/core:2.0
Caveats
- No matrix of OS versions. Only windows-2025 works.
- Absolute path is mandatory. C:\Program Files\WSL\wslc.exe is not on PATH in the running shell.
- Billing multiplier. Windows runners are 2x minutes. One short wslc job runs about 3 min wall clock -> 6 min billed.
- No Compose. If your CI uses docker-compose, rewrite the stack as individual wslc run commands.
Test matrix strategy
Because each wslc job boots a fresh utility VM, jobs are naturally isolated — no
shared state to clean up, no daemon restart races. Use a matrix to fan out tests
across images and treat each wslc run as an immutable unit:
strategy:
matrix:
image: [alpine, ubuntu:24.04, node:22-alpine, postgres:16-alpine]
runs-on: windows-2025
steps:
- run: |
$wslc = 'C:\Program Files\WSL\wslc.exe'
& $wslc run --rm ${{ matrix.image }} /bin/sh -c 'echo OK'
Four parallel windows-2025 jobs cost roughly the same billed time as one
sequential run, because each job is a separate 3-minute allocation. Add
wsl --update --pre-release at the start of every job — the runner
image ships stable WSL, and wslc only exists in the pre-release channel.
Getting logs and artifacts out
Container output is regular stdout, so capture it the same way you would any
CI command. To copy files out of a container use wslc container cp;
to snapshot a whole filesystem as a tarball use wslc export -o out.tar.
Both are covered in the cheat sheet.