Docker decided to charge a per user fee for companies and universities using their Docker Desktop for Windows package. If you are actually developing “Windows Containers”, this is the only way they can be built. If you want to develop Linux Containers using a Windows development platform, there are other fairly simple solutions that are completely free. When this paper was first written, the alternatives were in early stages of development, worked poorly, and required a lot of effort to install and use. Today all those problems have been solved, and there are several options that require almost no effort.
WSL Ubuntu 24.04 Distro
Docker has two components. The user interface is the “docker” command (a.k.a. docker-cli) that the user runs at the command line in a shell. All the work of building images and running tests is done in the background “docker engine” (a.k.a. “dockerd” or “containerd”) that runs as a background service under “systemd”.
The Windows Subsystem for Linux is the Linux-aspect of Windows. It allows a Windows user to run any Linux command from the Windows command line (cmd.exe or PowerShell) just by prefixing it with “wsl “. This runs the wsl.exe program in Windows, which then sends the rest of the command line to the default WSL Linux distribution by a process similar to SSH. Because Windows and WSL share all the Windows disks and files that the user is allowed to access, the current directory and environment variables in Windows are transmitted to Linux and become the default directory in which the Linux command runs.
Originally, WSL simply did not support “systemd” and therefore could not run the Docker Engine service that did all the work. It was possible to create a custom WSL system image that could run background services, and Docker Subsystem for Linux created such an image and depended on it for its operation. Slowly Microsoft added WSL support for “systemd” as a rather technical option you could turn on after Ubuntu-23.04, but even then, Ubuntu would tell anyone trying to install docker that they should get the Docker Subsystem for Windows instead. Starting with the WSL “Ubuntu-24.04” distro installed either using Windows Store or at the command line, “systemd” is turned on by default for everyone, and if you then issue the “sudo install docker.io” command, then the full normal Linux docker-cli and docker engine are installed.
If you configure Ubuntu-24.04 with docker.io installed as the default WSL distro, then in Windows cmd.exe, bat files, or PowerShell you can issue any docker command from Windows by running “wsl docker …” (or if you prefer, “wsl.exe docker …”). While the command is entered from Windows and the current directory is the Windows directory, docker runs in the WSL Ubuntu environment as a Linux command, and the images are stored in the docker engine image library.
The only remaining problem to overcome is understanding the state of networking if you execute an image with “docker run -p 8080:8080”. With Docker Desktop for Windows you run a docker.exe in Windows and there is a lot of effort put into connecting port 8080 in the container running in Linux to a proxy listening on port 8080 in Windows through a lot of special VPN logic written by Docker to make it all work. If you execute “wsl docker run -p:8080:8080” then the docker command runs in WSL Linux. The container port 8080 is then connected by the docker VPN to port 8080 on the WSL Linux distro. Now something lucky happens. Among the network addresses on the WSL Linux system is the “localhost” or loopback address of 127.0.0.1. WSL has a rule that when any program listens on a port on the localhost address, then Windows WSL causes a matching listen to be done on the Windows system localhost address. This means that any program running on the Windows host computer system can connect to localhost:8080 and that will in turn be passed to the program in the container. What would have worked on Docker Desktop for Windows, but won’t work transparently with WSL, is for a program on a different computer, or even on a VM running under Windows Hyper-V to connect to the container. I am not saying it cannot be done at all, but it requires a lot of extra work. So, running Docker under normal WSL without Docker Desktop is fine if all your test applications either run on Windows or in WSL itself.
You can create a PowerShell alias, or you can create a docker.bat file with contents
@ECHO OFF wsl docker %*
If you need it, you can add docker-compose the same way with “wsl sudo apt install docker-compose” and create another bat file for that command.
Alternately, you may decide to work in the WSL Ubuntu system using Visual Studio Code Remote for WSL. The editor runs in Windows, but docker runs in WSL. You can even install the Docker plugin in the Remote VS Code and it connects to the WSL docker.io that you installed above.
Podman Desktop for Windows
Podman is the Red Hat alternative to the standard Docker system. It is a redesign and reimagining of containers that is nearly 100% compatible with Docker, but with useful improvements and options. It is perfectly fine for building and testing applications.
Red Hat makes a free Podman Desktop for Windows available to anyone who wants to download it. It basically duplicates a lot of the ease-of-use features of Docker Desktop for Windows in terms of a GUI interface that will lead you step by step through the install and configuration process and provides a Windows based tool to display the images, containers, and volumes.
You can install Podman using “winget”. If you don’t want to read any documentation, do a
winget install redhat.podman-desktop
Then run it and it will lead you through the process of installing the podman programs in windows and creating a WSL distro with Fedora 40 and the podman service running in the background.
If you regard the Podman Desktop GUI as unnecessary fluff, you can just “winget install redhat.podman” to get all the programs, but then you have to manually create and start the WSL engine with the commands:
podman machine init
podman machine start
Because there is a podman.exe program running in Windows duplicating the docker.exe program of Docker (and with all the same commands and options), you can either change your existing programs to use “podman” instead, or you can create an alias by going to a directory early in your Path and creating a symlink named “docker.exe” that points to the “podman.exe” program:
mklink docker.exe "C:\Program Files\RedHat\Podman\podman.exe"
Then whenever you enter the “docker” command, path search will find the symlink and run podman.exe instead.
Podman has no elaborate VPN support, so it uses the built in Windows WSL trick of mapping “localhost” between Windows and all the WSL distros. Therefore, if you do a “podman run -p 8080:8080” you will only be able to connect to the container from a Windows program that connects to localhost:8080 or from a client program running in any WSL environment connecting to localhost:8080 (not from other machines or VMs).
Podman for Windows (History and Design)
The basic “container” support is a standard part of Unix and therefore Linux design. Any Unix program can, when it runs another program and creates a child process, restrict that child program from seeing and therefore accessing any directories, devices, or sources of information visible to the parent. The parent can also create and substitute alternate directories or dummy system objects for the child.
There are billions of ways this could be used. The company named Docker invented the modern Container by creating a standard highly restricted environment that could run almost any application program while restricting visibility to everything else that is going on in the machine.
With limited resources, they created a single environment (the docker engine) that could be used both by developers testing programs and by administrators running production containers on large servers or clusters of servers. To promise stability to their customers, they promised not to change that environment so that production programs would run reliably in the future.
Docker has been challenged as a production server standard by Kubernetes. Proprietary container runtimes have been added to Linux distributions like RHEL. In the cloud, vendors have created their own optimized runtime for AWS, Azure, and so on.
The Docker engine was designed to run as root and to maintain a common library of images without individual user access control. When each developer runs on their own machine and are admins, this is not as big a problem. It is obviously unacceptable for shared machines.
Red Hat Podman has the ability to run the engine as a background task under a logged in user. The container then runs with the developer’s own permissions, and each user has his own library of images and separate running containers. This makes container development work like any other program development.
However, when you are running on your own Windows machine and the engine runs under WSL, then the Docker Windows code is forced to run exactly the same as Podman for Windows. WSL forces the docker engine to run under your userid and store images separately for each user, and at the same time WSL forces Podman to run its engine under the Linux VM so podman.exe running in Windows has to connect to it through some remote virtual network protocol. Each starts from a different strategic design, but in Windows they are forced to work in exactly the same way.
The Podman engine does not speak the same protocol as the docker engine, so the VS Code Docker Plugin does not recognize it and cannot display useful information.
A Yale developer will see some extra useful commands and options in the following bat/powershell script that runs a container for testing:
podman secret create --replace deco.json SANDBOX.json podman run --replace --rm -it -p 8080:8080 --secret=deco.json --name tomcat iiq:8.4-yale-1.0.52
The “docker secret create” command only works with Swarm, but Podman makes it a normal command. Then they add --secret to the run command so you don’t have to use compose. They add --replace to both commands to delete and replace an existing secret or container with the same name before you create a new one.
Podman build supports Dockerfiles, although it also allows you to use the alternate name “Containerfile” to avoid brand name confusion.
A Container is just a type of Linux Process
A Linux program “running in a container” is just a program that has been started in a special way. It doesn’t look or behave differently from normal programs.
$ docker run --name tomcat -d --rm tomcat:jdk11-tc9 02f58c9115a4b0e35a22ec496cec9d5808e1e4394558a0491650d6e18d179a25 $ ps -ef|grep tomcat root 11797 11777 11 12:55 ? 00:00:01 /opt/java/openjdk/bin/java ... start
In this example we start a container that runs Tomcat, then display the running tomcat program as an ordinary Linux process. A Tomcat process looks pretty much the same whether it was started as a container, or as a background service under systemd, or interactively by a logged in user running the /bin/startup script.
The system services that create containers have always been part of Unix, but for decades they were obscure technical features. Then Docker released a software package to configure, build, and run application programs in a special configuration that became widely known as “Containers”. However, the Linux environment Docker decided to use to run applications is only one specific configuration of the more general idea of containerization.
A Docker application container appears to be a Linux system running in “single-user” mode. Nothing is running in the system except a single user login with its shell. You can boot any Linux system into single user mode, which is used to do maintenance on the system, but all images in Docker Hub create this appearance.
The original Unix design placed all system information in the structure you normally think of as “/”. While it contains directories and files, it also contains real disks, network disks, network adapters, sockets, the process created when a program runs, shared libraries loaded into memory, and so on.
When one program starts any other program, it can edit this “namespace” structure to remove objects it doesn’t want the new program to access and add synthetic objects. For example, it can remove all real LAN adapters but create an imaginary LAN backed by VPN software, so the new program can only access the VPN and not the real local network.
Containers are built on this capability. Docker invented a particular configuration and file format to create both a specific image of directories and files and a set of VPN dummy networks. This file format was then standardized by an industry working group.
Extra Responsibilities
At Yale, when an application runs on a VM it is the Linux Systems Group that installs and configures Java and Tomcat and installs prerequisite software. The developers only install the WAR into the Tomcat “webapps” subdirectory.
When developers build a container to run the application, they start with a base container from Docker Hub that typically contains a version of Java and Tomcat. They may provide the Yale root CA certificates in a cacerts file and the Yale Kerberos configuration in a krb5.conf file. The Dockerfile may run the package manager to install prerequisite shared libraries.
All containers can then run on a generic minimal Linux system. There is no need to customize a VM for any application. However, developers now need to know the system configuration files and packages that were previously the responsibility of the Systems group.
Alternatives
Docker tries to use a single implementation to work on a single user laptop, a large multiuser corporate server, a cluster of enterprise servers, and a Cloud based SaaS. Larger companies with more resources (Microsoft, Amazon, Red Hat/IBM) provided their own high-end alternatives for production use, while Podman is interesting for the developer’s laptop, which is the focus environment for this article.
At Yale, a developer cannot create production container images. The developer edits a Dockerfile project that generates images. A local image can be built and unit tested on the laptop. The Dockerfile project is then checked into git.yale.edu and a Jenkins job builds the production image and stores it on the Harbor image repository server.
Therefore, the rest of Yale does not know and cannot care what software is used on the laptop to test the Dockerfile project. It has to be compatible with the Dockerfile standard, but it can be Podman or any other compatible tool.
WSL is a different Container Runtime
Windows Subsystem for Linux is a tightly coupled Linux environment that can run Linux commands as if they were a natural part of Windows. Someone familiar with a Linux command like “grep” can use it on the Windows command line by prefixing the command name with “wsl”:
C:\Windows\System32>tasklist|wsl grep sql sqlwriter.exe 5804 Services 0 8,504 K sqlceip.exe 7780 Services 0 53,584 K sqlservr.exe 7848 Services 0 199,708 K
Under the covers, cmd.exe is piping the standard output of tasklist.exe to standard input of wsl.exe. The wsl.exe program communicates to the WSL Linux VM passing the current Windows environment (including the current directory), the grep command, and the stream of characters from the pipe. This sets up the environment, runs the Linux program (/bin/grep), and passes it the stream of characters as standard input. The program runs in a Linux system where all the Windows local disks have been mounted with file access permissions on the Windows files inherited from the user who executed the Windows command.
WSL is implemented as a single Linux VM running the Microsoft Linux kernel, on top of which a Microsoft container runtime supports multiple Linux “distribution” containers. You can install these WSL containers using Windows Store or wsl.exe. A subset of the available containers is:
>wsl -l --online The following is a list of valid distributions that can be installed. Install using 'wsl.exe --install <Distro>'. NAME FRIENDLY NAME Ubuntu Ubuntu Debian Debian GNU/Linux kali-linux Kali Linux Rolling Ubuntu-22.04 Ubuntu 22.04 LTS OracleLinux_9_1 Oracle Linux 9.1 openSUSE-Leap-15.5 openSUSE Leap 15.5
Microsoft tries to keep things simple, so they use the term “distro” to mean two different things that they assume the reader will not have to distinguish. In a list of install options, “OracleLinux_9_1” refers to an image tar file prepared by Oracle and submitted to Microsoft for distribution. When you do a “wsl --install”, this image file is used to create a WSL container, and the container is also given the name “OracleLinux_9_1”. After that, when the wsl documentation refers to running a program or an interactive shell in a “distro” they are talking about the container and not the image used to build it.
Over time the vendor may provide an updated image file under the same name. The “Ubuntu-22.04” name has been used for two years, but if you install it today and then display the /etc/lsb-release file you find it has “DISTRIB_DESCRIPTION="Ubuntu 22.04.3 LTS”, so this is probably the fourth refresh update of the image.
A WSL container is different from a Docker container. Docker containers are intended to be short term and lose all their disk data when the system shuts down. WSL containers have a Microsoft virtual hard drive file (a VHDX, like a VM disk), so changes are permanent until you discard the container.
WSL containers shut down and restart. Traditionally they run in single-user mode, but Microsoft changed the rules and today many of them, notably Ubuntu, will run in “systemd” mode if you add “[boot] systemd=true” to the /etc/wsl.conf file in the image file system disk and restart the container.
“wsl docker”
When WSL Ubuntu runs in the default single-user mode, it cannot install and run a Docker Engine. However, if you enable systemd, then Ubuntu will let you “sudo apt install docker.io” or even install docker as a snap. This puts a docker command in WSL Linux, and like any other command you can run from Windows by adding the wsl.exe prefix.
Since the primary topic of this paper is building Docker images, the specific Windows command of interest is
wsl docker build .
You execute this command from a Windows directory containing a Dockefile project. Windows invokes the Linux docker command in the same current directory, but in the Linux system. There it transfers data to the Docker Engine, which executes the operations in the Dockerfile, builds the image and stores it in the Engine’s local image repository.
If you don’t like typing “wsl docker”, you can create a docker.bat file in your path with the line:
wsl docker %*
This is the simplest way to install and run Docker on Windows without using any licensed material. You get Ubuntu from the Windows Store. You get docker from the Ubuntu package libraries. There is no docker.exe, but there is no need for one.
You could also install Podman in WSL Ubuntu, but you get a little more function (and a Fedora WSL container) if you install Podman into Windows instead.
Special Purpose WSL “distro” Images
The “wsl --import” command will create a WSL container from any correctly formatted tar file on your disk. Software packages can include such a tar file and use it to create WSL containers directly, bypassing the Microsoft distribution system.
Docker Desktop for Windows includes a tar file for their Moby Linux and creates a WSL container named “docker-desktop-data”.
Podman includes a Fedora 38 tar file and creates a container named “podman-machine-default”. You can then create additional WSL Fedora containers with different names using the “podman machine init” command.
Plumbing
This is a very complicated topic that must be explained precisely if you are to understand how things work. Otherwise, it will just appear as if testing containers works by magic.
When the WSL feature is activated, it creates a VM to run the Microsoft Kernel and the containers. That VM has a virtual LAN adapter, connected to a virtual network called “WSL”. A matching virtual LAN adapter is created in Windows connected to the same virtual network.
Windows sees its adapter as:
Ethernet adapter vEthernet (WSL): Connection-specific DNS Suffix . : IPv4 Address. . . . . . . . . . . : 172.21.16.1 Subnet Mask . . . . . . . . . . . : 255.255.240.0 Default Gateway . . . . . . . . . :
While all WSL Linux containers share a common eth0 adapter with its randomly chosen IP address:
2: eth0: inet 172.21.21.137/20 brd 172.21.31.255 scope global eth0
The eth0 adapter is configured to treat the Windows 172.21.16.1 address as the gateway. Therefore, any client connect from the WSL Linux containers to the Yale network or the Internet flows thorugh the Windows system and uses the networks to which the Windows host is connected at the time.
However, the connections work only one way. The Windows host system can connect to the WSL VM using the 172.21.21.137 address, but this is not visible to any computer outside of the Windows host. Therefore, WSL applications and containers can only be tested from the Windows host (and other WSL client programs).
The WSL containers all share the one VM LAN adapter. Port numbers on that adapter are owned by the first program in any WSL container to bind to them. A program running in WSL Ubuntu can get back a “port is being used by another program” error because a program in WSL Fedora is already using it.
Every system also has a Loopback virtual adapter, also known by the name “localhost” and the IP address 127.0.0.1. The loopback is entirely synthetic and in every other case is local to the one system. Loopback is handled by a device driver in the kernel. Here the Linux Kernel is shared by the WSL containers, so port numbers are owned by the first program in any WSL container to bind to it.
However, to allow Windows to easily communicate to WSL programs, the loopback driver in the Linux Kernel communicates to the loopback driver in the Windows kernel to extend the impression of a single shared device. A Windows Browser can communicate to a WSL Linux Web server by using a URL like “http://localhost:8080”.
Of course, using Internet protocol it is also possible to connect from Windows to WSL Web servers using the URL “http://172.21.21.137:8080”, but you have to discover this IP address and there is no nice hostname you can use instead of the address.
Suppose you run Tomcat as an ordinary Linux program (not in a container) in any WSL distro. It will bind to 0.0.0.0:8080, which means port 8080 on any LAN adapter it can see. WSL containers see two LAN adapters, so it will bind to loopback (127.0.0.1:8080) and eth0 (172.21.21.137:8080). A Widows Browser talks to Tomcat through a virtual network using the eth0 IP address, and through the internal Kernel to Kernel connection between the two loopback device drivers using “localhost”.
Now run Tomcat in a Docker or Podman container using the -p 8080:8080 parameter on the run command. Inside the container, Tomcat binds to 0.0.0.0:8080, but the container has a restricted namespace where the only network it sees is a VPN generated by Docker/Podman. A VPN is necessary because, unlike WSL containers, Docker containers are completely independent of each other. In theory, dozens of Docker containers can all bind to port 8080 on what they see as their LAN adapter and never get a “port is being used by another program” error. The containers use the VPN to talk first to the container Engine.
The Docker Engine in the WSL container named “docker-desktop-data” or Podman in the WSL container named “podman-machine-default” bind to 0.0.0.0:8080 and like Tomcat in the previous example, this becomes a bind to loopback (127.0.0.1:8080) and eth0 (172.21.21.137:8080).
A Windows Web Browser can connect to Tomcat in the container with the same URLs. The only difference is that the Browser communicates to the Container Engine, which forwards the data to the Docker container using the VPN.
The connection between the Windows Browser and any program running in WSL is a standard feature of WSL itself and did not depend on any additional code added by Docker or Podman, nor was it helped along by the docker.exe or podman.exe program. This means that if you decide to use the “wsl docker” or “wsl podman” approach and install nothing in Windows itself, you can still run containers with “wsl docker run -p …” or “wsl podman run -p …” and test then with a Windows Browser and a “http://localhost” URL.
Exposing the Docker API to Windows Programs
The Docker Engine exposes an API through a Unix socket to other programs running on the same Linux system. Podman exposes an identical API to allow third party Docker tools to work with it.
However, Unix sockets do not exist on Windows. Docker created a proxy program that runs in Windows and exposes the API using a Windows “named pipe”, and again Podman created its own proxy that works the same way. There are not a lot of programs that know how to talk to the named pipe, but the Docker plugin for VS Code is at least one example.
If you run Docker on a real Hyper-V Linux VM instead of WSL, you can configure the Docker Engine to listen for requests on port 2375 of a virtual LAN adapter. However, Docker does not provide authentication or access control, so the virtual network has to be entirely private inside your laptop.
This is one case where the Docker approach is so bad that Podman refused to duplicate it. You can use Podman on one machine to connect to Podman acting as an Engine on another machine, but it uses SSH. Security is provided by the SSH login, normally using public keys. Once the client Podman has done an SSH login, it can run a podman command as that user on the other system, which provides access to the images and files in that user’s home directory.
Configure the Docker Engine in a Hyper-V Linux VM
Create a Linux VM with Hyper-V Quick Create, Ubuntu Multipass, or download the ISO and create the VM from scratch. When it comes up, you can use sudo apt or sudo yum to install the normal docker.io or podman packages.
This is not the place to discuss Hyper-V networking in detail. There is a Default network that connects VMs to the host Windows. You can also create other networks, including ones connected to a real LAN adapter. You can always configure your VM to connect to more than one network, but then you have to configure a static IP address because it will be a server of sorts. Assume the address on the network you want to use is 192.168.2.204.
On the VM, edit the file /lib/systemd/system/docker.service. Find the line beginning “ExecStart=/usr/bin/dockerd”, and add a “-H” parameter followed by that IP address:
ExecStart=/usr/bin/dockerd -H fd:// -H 192.168.2.204 --containerd=/run/containerd/containerd.sock
save the file and then restart the docker.service or reboot the VM.
DOCKER_HOST
All versions of docker.exe (but specially the open-source one called “docker-cli” on GitHub and Chocolatey) will look for an Environment variable named DOCKER_HOST that points to a network address of a Linux machine running Docker (or Podman). To connect to the Engine configured above, the value of DOCKER_HOST should be ”tcp://192.168.2.204:2375”.
You can install both podman.exe and docker.exe on the same Windows system. However, you do not want DOCKER_HOST to be set when the podman.exe command or Podman Desktop Dashboard is running. In that case, it is better to set the environment variable locally where the docker.exe program is running. A solution may be a docker.bat file that looks like this:
@ECHO OFF set DOCKER_HOST=tcp://192.168.2.204:2375 set PATH=%PATH%;c:\dockercli\bin c:\dockercli\bin\docker.exe %*