FreeWilly logo
Windows 10 / 11 · Per-user install · Apache-2.0

Docker on Windows,
without Docker Desktop.

FreeWilly puts upstream Moby into a WSL2 distribution it owns, serves the docker_engine pipe every tool you already have looks for, and shows your containers in one window. Per-user install, no administrator prompt, and nothing left running that you did not ask for.

What it costs an agent
Taskbring this project's stack up and tell me why the api container is not responding
freewilly read context~150 tok
engine  running  wsl:freewilly  api=v1.43  pipe=docker_engine  ctx=default(ok)
web     up 6m       healthy   svc:shop/web   :8080→80   listening
db      up 6m       healthy   svc:shop/db    :5432→5432 listening
api     exited 137  ×3/2m     svc:shop/api   OOM  limit=512m
disk    images 14G (4.2G dangling)  volumes 2.1G (1 unused)
compose ./docker-compose.yaml → shop  3 svc, 3 present
cursor  c:4f21a0
freewilly read doctor api~200 tok
api OOM-killed ×3 · mem_limit 512m too low → raise it in compose
freewilly read logs api --dedup --budget 1500 --out .freewilly/logs/api.log~300 tok
1 unique line ×3 (deduped from 41), written to file — then Grep, paying for matches only
freewilly do compose up --wait~100 tok
shop 3/3 ready · api healthy in 4.2s (this one still asks — it writes)
freewilly read verify svc:shop/api~80 tok
:8080 answers from Windows · 200 OK · PASS
Docker today: 15–30 calls · 30–60k tokens · 1–3 interruptionsThis session: ≈5 calls · ~2–5k tokens · 0 interruptions

A scripted session — the costs are targets the benchmark must prove or falsify, acceptance criteria and not measurements. Steps 1, 2, 3 and 5 are reads, so one allowlist line — Bash(freewilly read:*) — removes every prompt on the inspection path while step 4 still asks.

🔓 Free at any headcount🚫 No telemetry, no account💤 Not a background service
.NET 10 · WinForms tray + WPF windowUpstream Moby 29.7.2, pinned by digestOne owned WSL2 distroZero NuGet Engine API client
Who operates this

The operator is an agent. You install and approve.

FreeWilly is a Docker installation whose primary operator is a coding agent. The agent runs, inspects and diagnoses; you install, approve and intervene. Every decision on the agent surface is judged in tokens rather than clicks — which is why the site opens on a session and not a screenshot.

AgentClaude Code
the freewilly CLI, over an ordinary shell
Run, inspect, diagnose, clean up after itself
Youat the keyboard
the installer, the tray, the container and log windows
Install, approve, intervene, uninstall

The desktop path is not sacrificed — the installer, the tray and the window are what it is for, and they landed before the agent surface did rather than after.

The design laws

Ten laws, in the order an agent meets them

Binding, in the same sense as the product's. A feature that breaks one is wrong even if it was asked for. Each names the defect it prevents.

P1

The shell is the surface

Every agent-facing capability is a freewilly CLI verb first. An MCP tool is a second head over the same method, or it does not exist.

P2

One call replaces a session

Learning what the machine is running is a product feature, not a docs problem. Needing six commands to learn the engine's state is a defect in FreeWilly.

P3

Tokens are a measured budget

Every response has a size ceiling and the canonical task has a measured cost. “It got cheaper” has to be a number, and a regression fails the build.

P4

A file beats a stream

An unbounded log read is the largest token sink here. Write it to disk and let the agent Grep it — it pays for the lines that match, not for the whole log.

P5

Names, not ids

A 64-hex id changes on every recreate. The address is the name — svc:<project>/<service>, or the container name — so ids stop being currency threaded across calls.

P6

Errors are instructions

Every refusal carries what was wrong, what is allowed, the nearest match, a correct example — and the Windows fact that explains it. An error that costs a round trip to read is a defect.

P7

Never surprise you

Read and write are split at the argv level so an allowlist can tell them apart. Destructive calls take a confirm token, and everything the agent creates is labelled with its session.

P8

The agent cannot see

Give it cheap textual proof that what it started works — the port listens from Windows, the mount resolved, the service answered — or every mistake costs a trip back to you.

P9

Session N+1 is cheaper than N

A cursor and a change feed, so a follow-up session reads the delta rather than re-deriving the whole machine from nothing.

P10

Compose, don't fork

The surface is a shape over the Engine API and facts Windows already knows. It is not a second Docker CLI and never grows a build, a push or a compose up of its own.

Why it exists

The engine is free. The desktop app is what costs.

Moby and the docker CLI are Apache-2.0 and always were. What a company pays for, and what a managed laptop cannot install, is the wrapper around them. FreeWilly is a different wrapper: it installs the same engine, gets out of the way, and stops when you tell it to.

🪪

No licence to count seats against

Free at any headcount, for any use, with no seat maths and no renewal date. That is the whole reason to try this instead of the thing everyone already has.

🔌

Nothing resident

No Windows service, no scheduled task, and autostart is off until you turn it on. The engine runs for exactly as long as something is running it — an engine that holds gigabytes from every boot is the complaint this project starts from.

🧍

Per-user, no admin prompt

Everything lands under %LOCALAPPDATA%\FreeWilly: the verified downloads, the distribution's disk, and the docker.exe that goes on your PATH. Which is what reaches a corporate laptop you are not an administrator on.

📦

Upstream, not a fork

Alpine's minirootfs, Docker's own static Linux binaries, Docker's own Windows CLI — each pinned to a version and a SHA-256 this project states itself. Nothing is patched, so nothing here can be blamed for how the engine behaves.

🧪

It checks before it copies

A preflight reads the Windows build, virtualization, the WSL2 kernel and any rival engine, and refuses the install while a blocking row is not green. Nothing is written to disk while the answer is no.

🗑️

An uninstall that is one command

The distribution is called freewilly and it is this tool's, never yours. Your own WSL distros are untouched, and removing the engine cannot take anything of yours with it.

Before anything is installed

Why Docker will not run here — in five rows

“It does not work on my machine” has five common causes on Windows, and they have five different remedies. freewilly --preflight names the one you have, prints the command that fixes it, changes nothing, and exits 1 so an installer can stop rather than fail halfway.

freewilly --preflight
FreeWilly preflight — what this machine can host

  [ok  ]  Windows build            Windows 10.0, build 26200
  [ok  ]  Hardware virtualization  enabled — a hypervisor is already running
  [FAIL]  WSL2                     not installed — wsl.exe is not on this machine
           -> Run `wsl --install --no-distribution` in an administrator terminal, then
              reboot.
  [ok  ]  Container engine         nothing else owns the docker command or the docker_engine pipe

1 row blocks an install. Nothing has been copied to disk.
  • Windows build — 19041 or later, because below it no amount of configuration gets a WSL2 kernel
  • Hardware virtualization — and it reads the hypervisor first: Windows reports the firmware bit as off once something has claimed it, so the naive order sends you into a BIOS to enable what is already on
  • WSL2 — missing wsl.exe, a half-installed feature with no kernel behind it, and “new distros default to WSL1” are three different states with three different lines
  • Container engine — anything else that owns the docker command or the docker_engine pipe, because two engines competing for one pipe leaves neither working
  • Docker context — where your own docker command points, because an engine that is running and a client aimed at something else fail with the same sentence, and only one of them is fixed by starting anything
  • Every row carries its remedy — the command that fixes it, wrapped and marked once, because repeating the arrow per line reads as several actions where there is one
  • Or as JSON — --json gives an installer the same report, verdicts and remedies included

The same check runs inside the installer, and the same report is what a clean Windows 11 virtual machine is driven through on the way to a release — because a red row nobody has ever executed is not a check.

Provisioning

Eleven steps, and it stops at the one that broke

Provisioning runs from an installer, where there is no terminal to answer a prompt in. So every step is unattended, every step is named, and the run stops at the first failure — a report listing six failures where there was one is a report nobody can act on.

1

Acquire, and verify

Five of the eleven steps, one per artefact: the Alpine root filesystem, the static Linux engine, the Windows CLI zip, and the Compose and Buildx plugins — each downloaded and checked against a digest recorded in this repository, not one served by the same host as the file. Already verified on disk means no second download.

2

Inspect, before touching WSL

The engine tarball's member list is read locally: do the entries share one top directory, and are dockerd, containerd, runc and the rest actually in there? A bad archive is caught before a distribution exists.

3

Import an owned distro

wsl --import freewilly … --version 2. A fixed name that is this tool's, so an apt upgrade or a wsl --unregister you ran for your own reasons cannot take the engine with it.

4

Install the engine inside it

One non-interactive sh script under set -e, so it stops where it broke: iptables and socat from apk, the binaries unpacked into /usr/local/bin, and systemd=false — nothing here is a service.

5

Place the Windows CLI

docker.exe is extracted to …\FreeWilly\bin. Putting that directory on your PATH is the installer's job, and this step's only job is being the path it points at.

6

Place Compose and Buildx

Both plugins go where the CLI looks for one, so docker compose up and docker buildx build work from the install rather than from a second download — pinned by digest like everything else here. Every build the daemon kept is a page in the window, which is also where the docker-desktop:// link buildx prints at the end of a build lands.

?

Or just look first

--plan prints every pinned version, digest and path and reaches nothing at all. --acquire downloads and verifies, and stops before WSL2 is touched — both change nothing outside this tool's own directory.

freewilly --help — the engine verbs
freewilly — put upstream Moby into a WSL2 distribution this tool owns.

  --plan        the pinned versions, digests and paths; reaches nothing
  --acquire     download and verify every artefact, and stop
  --provision   acquire, import the distribution, install the engine

  --run         start the engine and serve \\.\pipe\docker_engine until Ctrl+C
  --stop        stop the engine and terminate the distribution
  --status      what the engine is doing, by asking it
  --api         version and containers, read through the Engine API
  --watch       print /events as they happen, until Ctrl+C
  --autostart   on | off | status  - off unless you turn it on

  --help        this

Exit code 0 means the mode finished; 1 names the step it stopped at. For --status, 1 means the engine is not answering.
The transport

Your docker commands do not need to know

A Linux dockerd cannot create a Windows named pipe — that is a Win32 object. So something on the Windows side has to, or every shell and every script you already have needs a DOCKER_HOST. FreeWilly is that something.

WINDOWS WSL2 · freewilly docker.exe docker compose the FreeWilly window \\.\pipe\ docker_engine the relay, in the tray's own process ACL: your account only wsl.exe stdio socat dockerd /var/run/docker.sock No forwarded TCP port anywhere on this path — the Engine API is equivalent to root on the machine, and a port every local process can reach cannot express “only me”.
🔒

The ACL is the reason

The pipe is created for your account and nobody else. A forwarded port cannot say that, and full access to the Engine API is full access to the machine — so the hop runs over wsl.exe's stdio instead.

🧩

Your existing tools, unchanged

It is the same pipe name, so the CLI, Compose, Testcontainers, an IDE plugin and whatever your CI script does locally all find it without a setting. Nothing has to be told about FreeWilly.

🧵

An API client with no dependencies

The app talks HTTP over the pipe directly — a named-pipe stream handed to .NET's own HTTP handler, pinned to Engine API v1.43. No NuGet package, and no shelling out to docker.exe once per refresh.

In the tray

“Is Docker up?” should be a glance

The icon carries the engine state as a shape, and colour only reinforces it. At sixteen pixels a hue is a hint: two of these are seen at a glance, by people who may not separate red from green, against a taskbar that is light on one machine and dark on the next. These three are still distinguishable in a screenshot printed in black and white.

Running

A filled disc. And it means the engine answered — the state comes from the event stream's own connection, not from remembering that a start was clicked.

Starting

A ring with a bite out of it: the same outline as stopped and unmistakably not it, which is what tells “on its way up” from “not running” without relying on hue.

Stopped

A plain ring. Start engine is one click away in the menu, and it launches the engine in a process the tray does not own.

Deliberately short

Four items, because a tray menu that grows is a second app

  • Start engine / Stop engine — each enabled only when it would do something
  • Open window — the container list, and the one already open is brought forward rather than duplicated
  • Quit — and the engine keeps running. The asymmetry is the point: a database another process is using does not die because somebody closed an icon
  • The only thing that stops the engine is the menu item that says so
Start engine Stop engine Open window Quit 14:08 FreeWilly — engine running
The window

One window, and it is the list of containers

Because that is what the tool is opened for. Name, image, state, how long it has been up, and the ports — and the ports are links, because a published 8080 is the thing you actually wanted and retyping localhost:8080 is a small daily tax a GUI exists to remove.

FreeWilly ─☐✕ Engine running · Engine API v1.43 NAME IMAGE STATE STATUS PORTS web nginx:1.29-alpine running Up 12 seconds 8080->80/tcp postgres-dev postgres:17-alpine running Up 4 minutes 5432->5432/tcp redis redis:8-alpine running Up 4 minutes 6379/tcp api node:22-alpine exited Exited (1) 3 minutes ago A blue port is published and opens in your browser. Grey is exposed only — there is nowhere on this machine to send you, so it is plain text and not a link that lands on nothing.
Whatever your Windows is. The window is WPF on the built-in Fluent theme with ThemeMode="System" — light and dark follow the OS, with no extra package.
FreeWilly Engine stopped The engine is not running Start it to see your containers. Start the engine
The details

Correct without being asked, and empty on purpose

  • No refresh button. The list is a view of the engine: it reads /events as the daemon writes it, and only the events that change a container list cost a read — the rest would be a poll in disguise
  • Started in a terminal, shown here. A docker run in any shell appears without you touching the window
  • The stream re-opens itself after every break, so stopping and starting the engine does not leave a window quietly lying
  • Empty is a designed state, and the two reasons a list is empty read differently — only one of them is something you can act on, and that one offers the button
  • Duplicates folded. A port published on both address families comes back twice from the API and would otherwise be two identical cells
  • UDP is text. Only TCP gets a link, because http://localhost:x is not where a published UDP port is
  • Four destinations, and Builds is one. Containers, images, volumes and what has been built here — which is also where the docker-desktop:// link buildx prints at the end of every build lands, because the record it names is one the daemon kept and only the address was dead
Scope

What it is not

Five things this project has decided against, written down where they can be pointed at. A tool with no stated non-goals is a tool that will eventually be asked for all of them.

Feature parity with Docker Desktop

Kubernetes, extensions, a dashboard for everything. The list a user actually opens the app for is short, and stopping there is what keeps this small enough to trust.

A fork of the engine

Upstream Moby, unpatched, pinned by digest. If the engine misbehaves, that is between you and upstream, and this project has not touched it.

macOS and Linux

Linux already has the engine, and the entire mechanism here is WSL2 and a Windows named pipe. Portability would mean a different tool wearing the same name.

Telemetry, accounts or a sign-in

Nothing is measured, nothing is sent, and there is nothing to register. There is no build of this with an opt-out.

A resident background service

The complaint that sends people looking for an alternative is an engine holding gigabytes from every boot. Not being that is the product, not a preference.

Take the installer

One file, and it asks for no administrator

The release page carries the installer and the SHA256SUMS.txt to check it against, and nothing else to choose between. It installs into %LOCALAPPDATA%\FreeWilly for your account alone — which is what reaches a managed corporate laptop, the machine this project exists for. The WSL2 feature the engine needs may still want elevation of its own; the installer runs the preflight and says so rather than failing halfway through a download.

🪟 Windows 10 build 19041 or later, 64-bit📦 No prerequisite — the .exe carries its own .NET runtime🔒 SHA-256 published beside the file

Uninstalling removes what was installed and asks about what was created. The freewilly distribution holds every image, container and volume you have, so it is never deleted without a question — and an unattended uninstall keeps it.