Introduction
Over the past few days, I’ve been working on adding Docker support to the vagrant-wsl2-provider plugin. This turned out to be more challenging than expected, but the journey taught me a lot about WSL2’s internals, systemd initialization, and multi-distribution package management.
In this post, I’ll walk through the development process, the challenges we faced, and how we solved them to get Docker running on 8 different Linux distributions in WSL2 through Vagrant.
The Challenge: Docker Needs systemd
The first major hurdle was enabling systemd support in WSL2 distributions. Docker relies on systemd to manage the container daemon, but WSL2 doesn’t enable systemd by default. This is controlled by the /etc/wsl.conf
file, which must be created and configured before systemd will start.
Implementing wsl.conf Support
We needed a clean API for users to configure WSL2 distributions. After some experimentation, we settled on a dotted syntax that feels natural:
config.vm.provider "wsl2" do |wsl|
wsl.systemd = true # Enable systemd
wsl.default_user = "vagrant" # Set default user
wsl.hostname = "my-docker-vm" # Set hostname
# Or use the longer form for advanced options:
wsl.wsl_conf.boot.command = "service docker start"
wsl.wsl_conf.network.generate_hosts = false
end
Under the hood, this generates a proper /etc/wsl.conf
file:
[boot]
systemd=true
[user]
default=vagrant
[network]
hostname=my-docker-vm
The Restart Problem
Here’s where things got tricky. After writing the wsl.conf file, the distribution must be fully restarted for systemd to initialize. A simple wsl --terminate
wasn’t enough - we needed wsl --shutdown
, which stops all WSL2 distributions and restarts the entire WSL2 backend.
Initially, I overthought this and added complex systemd polling logic:
# Over-engineered version (don't do this!)
halt
start
sleep 5
max_retries = 30
retry_count = 0
loop do
result = check_systemd_status
break if result == "running"
retry_count += 1
sleep 1
end
But it turned out we only needed:
# Simple and effective
Vagrant::Util::Subprocess.execute("wsl", "--shutdown")
sleep 2
start
The real issue wasn’t systemd initialization time - it was missing the iptables
package!
The Missing iptables Mystery
After getting systemd running, Docker still failed to start with a cryptic error:
failed to start daemon: Error initializing network controller:
failed to register "bridge" driver: failed to create NAT chain DOCKER:
iptables not found
Interestingly, running vagrant reload
after the initial vagrant up
would make Docker work perfectly. This led us down a rabbit hole trying to fix the “restart problem” when the real issue was simple: iptables wasn’t installed.
Adding iptables
to the package list solved it immediately:
- name: Install required packages
apt:
name:
- ca-certificates
- curl
- gnupg
- iptables # This was the missing piece!
state: present
Multi-Distribution Support: The Right Way
Initially, we tried cramming all distribution support into a single Ansible playbook with numerous conditionals:
- name: Install Docker
package:
name: "{{ 'moby-engine' if ansible_os_family == 'Debian' else 'docker' }}"
when: # complex conditions...
This quickly became unmaintainable. We refactored to use OS-family-specific playbooks:
provisioning/
├── docker-debian.yml # Ubuntu, Debian, Kali (Moby)
├── docker-redhat.yml # AlmaLinux, Fedora (Podman)
└── docker-suse.yml # openSUSE (Docker)
The Vagrantfile automatically selects the right playbook:
playbook = case distro
when /Ubuntu|Debian|kali/i then "provisioning/docker-debian.yml"
when /AlmaLinux|Fedora/i then "provisioning/docker-redhat.yml"
when /openSUSE/i then "provisioning/docker-suse.yml"
end
node.vm.provision "ansible_local" do |ansible|
ansible.playbook = playbook
end
Much cleaner! Each playbook focuses on one OS family without conditional spaghetti.
Why Microsoft Moby for Ubuntu?
You might wonder why we use Microsoft’s Moby packages for Ubuntu instead of the standard docker.io
:
# Ubuntu: Microsoft Moby
- name: Install Moby Docker
apt:
name:
- moby-engine
- moby-cli
- moby-containerd
- moby-runc
# Debian/Kali: Standard docker.io
- name: Install Docker
apt:
name:
- docker.io
- containerd
The reason is Docker Desktop licensing. Some enterprises are cautious about using docker.io
in WSL2 because of Docker Desktop’s commercial licensing restrictions on Windows. While this likely only applies to the GUI (not the engine itself), using Microsoft’s Moby distribution provides a clearer licensing path for commercial use.
For Debian and Kali, we fall back to docker.io
since Microsoft doesn’t provide Moby packages for all Debian versions.
RedHat Family: Podman as Docker
On AlmaLinux and Fedora, installing the docker
package actually gives you Podman with Docker CLI compatibility:
- name: Install Podman (Docker compatible)
dnf:
name:
- podman
- podman-docker # Provides 'docker' command
This is RedHat’s preferred approach - Podman is daemonless and rootless by default, which is more secure. The podman-docker
package provides a docker
symlink, so users can run docker run hello-world
and it “just works” using Podman under the hood.
Testing Across 8 Distributions
The final test suite provisions Docker on 8 different Linux distributions:
$ vagrant status
docker-almalinux8 running (wsl2)
docker-almalinux9 running (wsl2)
docker-debian running (wsl2)
docker-fedoralinux42 running (wsl2)
docker-ubuntu running (wsl2)
docker-ubuntu2404 running (wsl2)
docker-kalilinux running (wsl2)
docker-opensusetumbleweed running (wsl2)
Each one successfully runs the hello-world container:
$ vagrant ssh docker-ubuntu2404 -c "docker run hello-world"
Hello from Docker!
This message shows that your installation appears to be working correctly.
What’s Next for v0.2.0
Docker support is just the beginning. Here are additional features planned for the v0.2.0 release:
WSL Mount Support
Enable mounting Windows directories and VHD/VHDX data disks:
config.vm.provider "wsl2" do |wsl|
wsl.mount "D:\\data", "/mnt/data"
wsl.attach_vhdx "D:\\backups\\linux-data.vhdx"
end
This will allow users to:
- Share data between Windows and WSL2
- Back up Linux data to portable VHDX files
- Separate system and data for easier snapshots
Vagrant Snapshot Support
Implement Vagrant’s snapshot functionality:
vagrant snapshot save docker-vm baseline
vagrant snapshot restore docker-vm baseline
vagrant snapshot list
This will leverage WSL2’s export/import functionality to create point-in-time snapshots of entire distributions.
Lessons Learned
-
Start simple - My complex systemd polling was unnecessary. The real issue was a missing package.
-
Test early, test often - Running
vagrant destroy && vagrant up
repeatedly helped identify the iptables issue that only appeared on fresh installations. -
Separate concerns - OS-specific playbooks are much easier to maintain than one giant conditional playbook.
-
Read error messages carefully - “iptables not found” was right there in the logs, but I focused on the wrong problem initially.
-
WSL2 is not a VM - It has unique behaviors (like needing
wsl --shutdown
for systemd) that require understanding its architecture.
Try It Yourself
The vagrant-wsl2-provider with Docker support is available on GitHub:
git clone https://github.com/LeeShan87/vagrant-wsl2-provider.git
cd vagrant-wsl2-provider/test/docker-test
vagrant up docker-ubuntu2404
vagrant ssh docker-ubuntu2404 -c "docker run hello-world"
Conclusion
Adding Docker support to the Vagrant WSL2 provider was a journey through WSL2 internals, systemd initialization, and cross-distribution package management. The final solution is elegant: a simple configuration API, clean OS-specific playbooks, and reliable Docker/Podman installation across 8 Linux distributions.
The v0.2.0 release will bring even more functionality with VHD mounting and snapshot support. Stay tuned!
Have questions or suggestions? Open an issue on GitHub or reach out!