wslcontainers
Ask AI wslc 2.9.4.0

wslc CI/CD: run containers in CI without Docker Desktop

Updated 2026-07-27 · 8 min read · wslc public preview

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

AspectwslcDocker
Install time~30 s (wsl --update)~2 min (Docker Desktop install)
LicenceFree (built into Windows)Paid on Windows Server
DaemonDaemonlessdockerd background process
PATHAbsolute path requiredAuto-added to PATH
ComposeNot availableBuilt-in
Runner OSWindows Server 2025 onlyWindows 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.20
  • debian:bookworm-slim
  • ubuntu:24.04
  • python:3.12-slim
  • node:22-alpine
  • nginx:alpine
  • redis:7-alpine
  • postgres:16-alpine
  • busybox:latest
  • mcr.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.

Related guides