I wanted to share my experience setting up an environment for development with containerized applications on Windows. The license for Docker Desktop is not expensive, but it’s not free anymore, so I wanted to check if there’s an easy and cheap alternative to Docker Desktop for Windows.
My research is based upon these two resources:
- Install Docker on Windows (WSL) without Docker Desktop, by Jonathan Bowman.
- Replacing Docker Desktop with Multipass, to avoid Docker Desktop fees, by David Herron.
but my goal here is to go straight to set up a quick environment for working without too much hassle. No ceremony, not too much prose, just the real cake. In fact, if you already have a WSL Debian-based distribution you can skip to the “Windows side…” section and start reading from there.
I assume you have some basic knowledge of Docker and Linux. PowerShell and bash basic skills will be useful, too.
The game plan
The main goal here is to set up a containers runtime using WSL2, Ubuntu Linux and free packages. The secondary goal is being able to use it from both Linux and Windows.
To do so, we need to setup WSL2, Ubuntu, dockerd and containerd, then build docker-cli for Windows and finally wire them so they can talk to each other.
Pre-requisites
First, you need to set up your Windows so that you can use virtualized containers, as well as Windows Subsystem for Linux. Open a terminal and type:
Enable-WindowsOptionalFeature -Online -FeatureName 'Containers' -AllEnable-WindowsOptionalFeature -Online -FeatureName 'Microsoft-Hyper-V' -AllEnable-WindowsOptionalFeature -Online -FeatureName 'VirtualMachinePlatform' -AllEnable-WindowsOptionalFeature -Online -FeatureName 'Microsoft-Windows-Subsystem-Linux' -All
Winget
To install software, I will be using winget
, the package manager for Windows, since it’s the easiest way to install stuff on Windows these days (given there’s a package installer for it).
Windows Terminal
We will use console a lot, so I recommend using a decent terminal. Windows offers the excellent Windows Terminal, which can be obtained from Microsoft Store.
Note: Windows Terminal requires Windows 10 1903 (build 18362) or later
Powershell
Now we have the basics up and running, you should also install Microsoft PowerShell (not Windows PowerShell) to make your (console) life easier. Open a terminal session and type:
winget install --id 'Microsoft.Powershell' --scope machine
Ubuntu linux
You could also use any Debian-based installation, but I feel more comfortable in Ubuntu, so I’ll stick to this distribution.
PS> winget install Ubuntu
Once it is installed, just type:
PS> ubuntu
You will see a message like this:
Installing, this may take a few minutes
Once it finishes, you must create a default account. You can use whatever username you like; I usually use my first name, fernando
, and an easy to remember password.
Please create a default UNIX user account. The username does not need to match your Windows username.
Ok, once you are done, you’ll see something like the following:
Installation successful!
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.10.60.1-microsoft-standard-WSL2 x86_64)[...]
Now you have a Ubuntu Linux under WSL2 ready to play with. For now, just close the terminal with
exit
Meanwhile, on the Windows side…
Open a Windows Powershell (using Windows Terminal), and type the following to check everything is fine from the WSL perspective:
wsl --list
You should see the following:
Windows Subsystem for Linux Distributions:
Ubuntu (Default)
We will need also a folder to hold the docker-cli executable for Windows. If you already have one and it’s in your PATH, that’s fine. You’ll need it later. In case you don’t have one, let’s create one, for example: c:\tools
, and add it to the PATH (for example, using this).
Install docker in WSL
Open a terminal window in WSL Ubuntu (again, you can use Windows Terminal)
Start by upgrading the packages:
sudo apt update && sudo apt upgrade
[…takes a while…]
Install pre-requisites
sudo apt install --no-install-recommends apt-transport-https ca-certificates curl gnupg2
Let’s make sure the certificate for Docker repo is trusted:
source /etc/os-release
curl -fsSL https://download.docker.com/linux/${ID}/gpg | sudo apt-key add -
Add the Docker repository to the list of available repos for packages:
echo "deb [arch=amd64] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" | sudo tee /etc/apt/sources.list.d/docker.list
Now update the packages and install Docker and Docker CLI
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io
Add your user to the Docker group, named docker:
sudo usermod -aG docker $USER
Let’s create a configuration for the dockerd
daemon:
sudo mkdir /etc/docker/
sudo vi /etc/docker/daemon.json
And add the following contents:
{
"hosts": [ "tcp://0.0.0.0:" ],
"tls": false
}
You can launch dockerd
directly:
sudo dockerd
and stop with CTRL-C
or launch dockerd
in the background:
sudo dockerd &
And stop it with:
sudo pkill dockerd
If you launch it in the foreground, just open a new Ubuntu terminal session to keep on firing commands.
Check the containerd
process is listening:
ss -peanut
And expect something like:
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
tcp LISTEN 0 4096 *:2375 *:* ino:41752 sk:2001 v6only:0 <->
Try everything’s fine in the Docker world with this command:
docker -H 127.0.0.1 run --rm hello-world
If you are successful, export DOCKER_HOST
so you don’t have to type it every time:
export DOCKER_HOST="tcp://127.0.0.1:2375"
Extra. Avoid being asked for superuser password each time you run dockerd
As Jonathan Bowman suggests, you can avoid being asked for sudo
password each time you launch dockerd
.
sudo visudo
and add at the end of the file:
%docker ALL=(ALL) NOPASSWD: /usr/bin/dockerd
Build docker-cli for Windows
To build docker-cli for Windows we are going to use Docker, as suggested in their repo.
First, we need to clone the repo from Github:
git clone https://github.com/docker/cli.git
cd cli
Now, build Docker CLI for the Windows platform:
docker buildx bake
docker buildx bake --set binary.platform=windows/amd64
If everything goes well, you will find the docker-cli executable in the build
folder. You just have to move it from there to your Windows local tools location (we set that up previously, remember?). In our case, C:\tools\
cp build/docker-windows-amd64.exe /mnt/c/tools
Back in the Windows zone
Let’s get back to Windows PowerShell. Move to your tools folder and rename the docker-windows-amd64.exe to simply docker.exe, so that we just have to type docker (remember your tools folder must be in your PATH)
C:\
cd c:\tools
ren docker-windows-amd64.exe docker.exe
We need to know the IP of the WSL host. To do so, we can use this command (taken from here):
wsl -- ip -o -4 -json addr list eth0 `
| ConvertFrom-Json `
| %{ $_.addr_info.local } `
| ?{ $_ }
With the obtained IP, try the following (replacing with your IP, of course):
docker -H <YOUR_WSL_IP> ps
Did it work? Congratulations. You can try something more challenging:
docker run --rm hello-world
Recent versions of Docker CLI allow the creation of contexts, which allow you to create different contexts for different Docker environments. We can benefit from this to create a specific context for WSL:
docker context create wsl --docker "host=tcp://<YOUR_WSL_IP>:2375"docker context use wsldocker run --rm hello-world
The only problem I have now is that WSL changes instance IP on each reboot, so you have to change the context each time you restart. You can change context like this:
docker context update wsl --docker "host=tcp://<NEW_WSL_IP>:2375"
A simple PowerShell script to update the context automatically so that it is set to the correct host:
$wslip = wsl -- ip -o -4 -json addr list eth0 | ConvertFrom-Json | ForEach-Object { $_.addr_info.local } | Where-Object { $_ }$ctx = docker context ls --format json | ConvertFrom-Json | Where-Object { $_.Name -eq "wsl" }if($null -eq $ctx) {Write-Host "Creating Docker context 'wsl' to host=tcp://$($wslip):2375"docker context create wsl --docker "host=tcp://$($wslip):2375" | Out-Null} else {docker context update wsl --docker "host=tcp://$($wslip):2375" | Out-Null}docker context use wsl | Out-Null