Skrog runs the engine; you still need a docker command to talk to it. Docker
Desktop's docker.exe works fine (Skrog coexists with it), but if you want to
remove Docker Desktop entirely, Skrog can install the upstream command-line
tools itself:
skrog cli install
That fetches, checksum-verifies, and installs the exact versions pinned in this build:
| Tool | What it is | Where it lands |
|---|---|---|
docker |
the Docker CLI | <state>\bin\docker.exe (on PATH) |
docker compose |
Compose v2 plugin | ~/.docker/cli-plugins\docker-compose.exe |
docker buildx |
Buildx plugin | ~/.docker/cli-plugins\docker-buildx.exe |
docker-credential-wincred |
Windows credential helper | <state>\bin (on PATH) |
Nothing is fetched as "latest": the versions and per-download SHA-256 are compiled
into the Skrog binary, exactly like the engine (see PLAN.md §04).
The bytes come from each upstream project's own release assets — the docker CLI
from download.docker.com, compose/buildx/wincred from their GitHub releases —
so no Docker Desktop and no third-party mirror is involved.
skrog cli install adds the bin directory to your user PATH (no elevation)
and prepends it, so a new terminal resolves docker to the bundled CLI. Verify:
docker version
docker compose version
docker buildx version
Another docker on PATH may win over the bundled one. skrog doctor flags it
and says what will actually move it:
[warn] bundled docker CLI: another docker shadows the bundled CLI on PATH
bundled: ...\Skrog\bin\docker.exe
active: C:\Program Files\Docker\Docker\resources\bin\docker.exe
active is on the SYSTEM PATH, which always resolves first
Which PATH the other one is on decides what fixes it, and doctor says which:
- User PATH — open a new terminal; a PATH change only reaches shells started after it. If it persists, that entry sits ahead of Skrog's; move Skrog's first.
- System PATH — a new terminal will not help, and neither will reordering
your user PATH. Windows resolves the whole system PATH before the whole user
PATH, and
skrog cli installwrites the user one so that it needs no elevation. Removing the directory from the system PATH, or removing the docker.exe in it, needs an administrator. - Neither — nothing in the registry puts it there, so something in your shell's profile does.
Pass --no-path to install the tools without touching PATH (you add the directory
yourself).
docker build --ssh default works against Skrog, and Skrog does nothing to make
it work — which is the point. buildx dials the Windows OpenSSH agent
(\\.\pipe\openssh-ssh-agent) itself, and the forwarding channel rides the
ordinary API connection, so it crosses the bridge like every other request.
Private Go modules, npm and pip dependencies pulled over git all behave as they
do on a Linux engine.
The one thing that trips people is host-side: Windows ships the ssh-agent
service disabled, and the failure arrives at the end of a build rather than
before it:
ERROR: failed to convert agent config {default [] false}: invalid empty ssh agent
socket: Windows OpenSSH agent not available at \\.\pipe\openssh-ssh-agent.
skrog doctor warns about this before you spend a build on it. To fix it, in an
elevated shell:
Set-Service ssh-agent -StartupType Automatic
Start-Service ssh-agent
ssh-add # back in your own shellNo administrator? Two ways through without the service: point SSH_AUTH_SOCK at
another agent, which buildx prefers over the pipe when it is set, or hand buildx
the key directly with docker build --ssh default=C:\path\to\key.
Building for another architecture on the default builder fails, and the error does not say why:
$ docker buildx build --platform linux/arm64 .
#5 [2/2] RUN uname -m
#5 0.224 exec /bin/sh: exec format error
That reads like a broken Dockerfile or a bad base image. It is neither. The
default docker driver builds inside dockerd's own BuildKit, which carries no
emulator, so a foreign RUN needs a binfmt_misc handler registered on the
host — and a stock WSL2 machine has none.
A container-driver builder needs nothing installed, because official BuildKit images bundle the emulators:
docker buildx create --name cross --driver docker-container --bootstrap
docker buildx build --builder cross --platform linux/arm64 .
# aarch64Measured on this engine: the build succeeds, and the host's binfmt_misc table
is untouched afterwards — the emulators live inside the builder container
(/usr/bin/buildkit-qemu-aarch64 and friends), not on your machine.
skrog doctor reports which case you are in, as information rather than a
warning: most people never cross-build.
Docker Desktop does — its docs say multi-platform builds work "by default... using the QEMU that's bundled within the Docker Desktop VM" — so this is one step Desktop does not ask of you.
Registering handlers is not a distro-local act, though. Every WSL2 distro
shares one utility VM and one binfmt_misc table — same kernel, same
boot_id — so handlers Skrog registered would change how your Ubuntu executes
foreign binaries too, and would silently replace any that tonistiigi/binfmt
or your distro's qemu-user-static had put there. That is the same category as
~/.wslconfig, which skrog wsl-config apply treats as
needing your consent rather than doing on your behalf.
So it stays a thing you opt into — and for builds, the one command above is the answer for almost everyone and needs no opt-in at all.
Everything above is about docker build. Running one is a different problem
with a different answer, and the buildx recipe does nothing for it — a builder
builds; it cannot run a container.
docker run --rm --platform linux/amd64 alpine uname -m
# exec /bin/uname: exec format errorThis matters most on Windows on ARM, where the majority of images on Docker Hub are amd64-only. Pulling one works; starting it does not.
Skrog can register the interpreter for you, and does not until asked:
skrog config set emulation.platforms linux/amd64
skrog restartThat is the whole setup. The same command now works:
docker run --rm --platform linux/amd64 alpine uname -m
# x86_64Both directions work, and --platform takes either spelling of the
architecture — linux/arm64 and linux/aarch64 are the same thing to docker:
# on an amd64 machine, with emulation.platforms = linux/arm64
docker run --rm --platform linux/aarch64 ubuntu:26.04 uname -m
# aarch64You only ever name the other one. Skrog refuses to register a handler for the architecture you are already running on, because routing native binaries through QEMU would slow everything down for no benefit:
PS> skrog config set emulation.platforms linux/amd64 # on an amd64 machine
skrog: amd64 is this machine's own architecture; it needs no emulation.
Registering an interpreter for it would route native binaries through
QEMU and slow everything down for no benefit.
The key takes a comma-separated list, but with emulators shipped for
linux/amd64 and linux/arm64 only, one of the two is always your own — so in
practice there is exactly one value to set, and it is the other architecture.
Compose picks this up with nothing extra; platform: on a service is the same
mechanism:
services:
arch:
image: ubuntu:26.04
platform: linux/arm64
command: uname -m # aarch64The engine rootfs already carries the emulator — qemu-x86_64 in the arm64
rootfs, qemu-aarch64 in the amd64 one — so nothing is downloaded. The
handler is registered on every engine start, because binfmt_misc lives in
the kernel and wsl --shutdown wipes it, and it is removed again on
skrog stop and on uninstall.
Read the consent paragraph above before setting it: this is the change that reaches your other distros, which is exactly why it is a key you set rather than a default you discover.
Two things worth knowing:
- It is slow. QEMU user-mode emulation is several times slower than
native. Fine for
apk addand a smoke test, painful for a compile. - What changes is not only what you asked for. An amd64-only image that
fails fast today will start succeeding slowly instead, with nothing
announcing it.
skrog doctorreports which handlers are live.
Skrog ships emulators for linux/amd64 and linux/arm64 only. Those are what
images are really published for; the architectures below them are a list of
things that exist rather than things anyone runs on a Windows laptop.
The bundled tools are open source and redistributable: the docker CLI, Compose,
and Buildx are Apache-2.0; the credential helper is MIT. Each project's LICENSE is
installed alongside the binaries in <state>\bin\licenses. Skrog is not
affiliated with or endorsed by Docker, Inc.; "Docker" is a trademark of Docker,
Inc.
skrog cli uninstall
removes the bundled tools and takes the bin directory back off your PATH. (Plain
skrog uninstall removes the engine; the CLI bundle is managed separately so you
can keep the CLI while reinstalling the engine.)
Both architectures get all four tools, but not from the same place, and the difference is worth knowing before you audit them.
| tool | amd64 | arm64 |
|---|---|---|
docker |
Docker's published static build | built by Skrog from docker/cli source |
compose |
upstream release | upstream release |
buildx |
upstream release | upstream release |
docker-credential-wincred |
upstream release | upstream release |
Upstream publishes no Windows arm64 docker.exe. download.docker.com's
static tree contains exactly one directory, x86_64/, and docker/cli
attaches no binaries to its releases at all. So on Windows on ARM,
skrog cli install used to lay down compose, buildx and the credential helper
— all of which do have real arm64 builds — and skip the one command anybody
types (#450).
docker/cli is Apache-2.0, pure Go and fully vendored, so Skrog builds it:
from a tag pinned to an exact commit, in the same pinned Go toolchain as the
engine, with SLSA build provenance and a cosign-signed checksum, published as
a dockercli-v* release of this repository. third_party/docker-cli/ is the
whole of it.
And you can reproduce it. The build is deterministic — no timestamp, no
build host in the output — so running third_party/docker-cli/build.sh on
any machine with Docker produces a byte-identical binary. First verified
2026-09-21: a local build and the CI build of commit 4a63305d both came out
at 910087c0d9a80ac0e7802f8c48b98cdc7fd890c78e0dab5f0472bee856ac50a0,
28,319,232 bytes.
That is a stronger answer than the provenance attestation on its own. The attestation says we built it; reproducibility means you do not have to take our word for what from.
amd64 keeps coming from Docker, deliberately:
- Anyone can check it. Those bytes are Docker's own. Re-download the zip
and compare it against the
sha256pinned ininternal/dockercli/manifest.json, with no reference to Skrog. A binary we build can only be checked against us. - Blast radius. Building it ourselves would put every user behind our build, rather than only the arm64 users who have no alternative.
- A bump stays a URL and a hash, not a rebuild.
It is not about code signing. Docker's published Windows CLI is not
Authenticode-signed either — measured on the binary skrog cli install places
— so nothing is being preserved on one side and lost on the other.
When upstream publishes a Windows arm64 docker.exe, the manifest points at
it, third_party/docker-cli/ is deleted, and both architectures are Docker's
again. This is a stopgap with an exit condition, not a component Skrog has
taken ownership of.
The arm64 build reports the pinned version and the upstream commit it was
built from. It deliberately does not claim Docker Engine - Community,
which is Docker's build string for Docker's builds — that field is left empty
rather than filled in with something untrue.