CI runners
On this page
Skrog turns a Windows machine into a Linux-container CI runner: the engine is
upstream dockerd in WSL2, so a job that runs on docker on a Linux runner runs
here too, with no per-runner Docker Desktop license and no auto-update that
changes the engine under a pipeline.
There are two halves to this, and they are independent:
- Installing Skrog on a runner — setup-skrog
for GitHub Actions, the same script in
before_scriptfor GitLab, or a baked image (seecontrib/). - Running jobs against the engine — everything below. Anything that follows
dockerfollows Skrog:DOCKER_HOST, or theskrogdocker context.
Point a runner at the engine
Which endpoint Skrog serves depends on whether Docker Desktop is in the way:
| Docker Desktop | pipe Skrog serves | what plain docker does |
|---|---|---|
| not installed — the usual runner | \\.\pipe\docker_engine | works, no flag and no context |
| running, holding the default pipe | \\.\pipe\skrog_engine | reaches Desktop; use the skrog context |
So on a runner you normally need nothing at all: Skrog serves the pipe docker
already talks to.
The install also wires a docker context named skrog, and setup-skrog exports
DOCKER_CONTEXT=skrog for the rest of the job. That form is correct on either
machine, because the context points at whichever pipe Skrog actually took — which
is why it is what the action exports rather than a hardcoded host.
For a tool that does not read docker contexts, ask Skrog what it is serving rather than hardcoding a pipe name:
$env:DOCKER_HOST = (skrog status --json | ConvertFrom-Json).endpoint.dockerHost
skrog status reports the endpoint the running supervisor actually bound,
so it is right on either machine; plain skrog status prints it too. The
equivalent via docker is docker context inspect skrog --format '{{.Endpoints.docker.Host}}', which asks docker what Skrog told it — the same
answer, one step further away.
GitHub Actions (self-hosted Windows runner)
jobs:
build:
runs-on: [self-hosted, windows, skrog]
steps:
- uses: actions/checkout@v4
- uses: wslkit/setup-skrog@v2
with:
version: 0.4.0 # pin it; "latest" resolves the newest release
- run: docker run --rm alpine:3.20 echo hello
The runner needs a logged-on interactive session — WSL2 cannot start from a
Windows service. See auto-logon-runner.md; skrog runner check gives one verdict on whether a host is set up correctly
(auto-logon and its account, autostart, the supervisor, engine health, and
whether the machine sleeps on mains power).
Two things GitHub-hosted runners get wrong for this workload and a Skrog runner
gets right: the engine is pinned by you (a committed skrog.lock, so laptop
and runner install the same dockerd, containerd, runc and BuildKit to the
commit), and Linux containers run natively rather than needing a Linux runner
in the fleet.
GitLab CI
Shell executor (simplest)
The Windows runner runs jobs in a shell; Skrog is just the engine docker
talks to:
default:
before_script:
- Invoke-WebRequest https://raw.githubusercontent.com/wslkit/setup-skrog/v2/scripts/install-skrog.ps1 -OutFile install-skrog.ps1
- pwsh -File install-skrog.ps1 -Version 0.4.0
- $env:DOCKER_CONTEXT = 'skrog'
build:
script:
- docker run --rm alpine:3.20 echo hello
On a baked image (contrib/packer) drop the before_script entirely.
Docker executor (jobs run inside containers)
Point the executor at the engine’s pipe in the runner’s config.toml:
[[runners]]
name = "windows-skrog"
executor = "docker"
[runners.docker]
# The pipe Skrog took. On a runner without Docker Desktop that is
# //./pipe/docker_engine; confirm with `skrog status`, which names the
# endpoint the running supervisor bound.
host = "npipe:////./pipe/docker_engine"
image = "alpine:3.20"
privileged = false
volumes = ["/cache"]
Confirm the host with skrog status, and note that the runner
service still needs the interactive session that keeps WSL2 alive — the
executor talks to the engine, but the engine is per-user.
gitlab-runner exec was removed in 17.0, so a pipeline cannot be run locally
with the real runner any more; use gitlab-ci-local (below) for that.
Testcontainers
Testcontainers needs no special configuration beyond DOCKER_HOST. Two things
about it are worth stating, because both were bugs in Skrog before they were
features:
- Mapped ports are reached from Windows (
container.MappedPort+container.Host), which needsengine.userland-proxy=false— Skrog’s default. See vpn.md. - Ryuk, the reaper container, mounts the engine’s socket
(
-v //./pipe/...:/var/run/docker.sock). Skrog maps a Windows named pipe in a bind mount to the engine’s own/var/run/docker.sock, so this works as it does on Docker Desktop. NoTESTCONTAINERS_RYUK_DISABLEDneeded.
# On a runner Skrog holds the default pipe, so nothing is needed:
go test ./...
# Alongside Docker Desktop, point at Skrog explicitly:
$env:DOCKER_CONTEXT = "skrog"; go test ./...
The acceptance suite runs a real Testcontainers module (Go, Ryuk enabled)
against the engine: test/e2e/testcontainers.
Running pipelines locally
The point of a local engine is that a pipeline can be debugged locally, not
just executed in CI: act for GitHub Actions, gitlab-ci-local for GitLab,
Dagger, and a BuildKit cache a laptop and a runner can share. All of it — with
measured numbers and each tool’s own limitations — is in
local-ci.md.
Housekeeping on a long-lived runner
A runner that never reboots accumulates images, build cache and volumes:
skrog prune --all --until 24h # reclaim, with a report
skrog reset --to clean-slate # restore a snapshot between jobs
skrog prewarm images.txt # pull the pinned list ahead of need
skrog doctor warns before the data disk runs out (disk.warn-below is
configurable), which is the failure that otherwise shows up as an opaque
mid-build error. See housekeeping.md and
snapshots.md.
What is not supported
- Windows containers. Skrog runs Linux containers via WSL2; a job that needs Windows containers needs a Windows-container engine.
- Rootless / multi-user on one host. The engine is per-user, in that user’s interactive session. One runner account per host.
- Running as a Windows service without a session. WSL2 will not start; this is why auto-logon exists.