Featured image of post Lightweight Docker in WSL without Docker Desktop

Lightweight Docker in WSL without Docker Desktop

How to run Docker without Docker Desktop

Introduction Link to this section

Docker Desktop provides many features, but it is also heavy and slow to start. This made me remove it and install only Docker Engine directly in WSL, reducing startup time from minutes to seconds.

In this post I’ll show how to do it, some configuration tips, how to share the Docker socket across WSL distros (like Rancher Desktop and Podman Desktop do automatically), and a comparison with those alternatives.

Filesystem performance warning: bind-mounting source code from /mnt/c (Windows drives) into containers is roughly 10Γ— slower than using the WSL ext4 filesystem at ~/. The 9P protocol crossing the VM boundary is the bottleneck. Keep your repos under ~/ inside WSL and access them from Windows via \\wsl.localhost\Ubuntu\home\you\repo.

Terminology Link to this section

  • Docker Engine: The core component of Docker, providing orchestration of the container runtime, image management, networking, and storage capabilities.
  • Docker CLI: The command-line interface to interact with the Docker API.

Why do this? Link to this section

The main motivations for running Docker Engine directly in WSL instead of Docker Desktop, Rancher Desktop, or Podman:

  • License cost: Docker Desktop requires a paid subscription for companies with 250+ employees or $10M+ annual revenue. Docker Engine is Apache 2.0 β€” free for any org size. Rancher Desktop and Podman Desktop are also free, but have architectural trade-offs (see comparison).
  • Startup time: Docker Desktop can take minutes to start. Docker Engine in WSL starts in ~14 seconds with systemd, or near-instantly with a profile script.
  • Resource usage: no GUI, no extra VM layer, no bundled Kubernetes. Just dockerd running as a systemd service (~100MB RAM, ~1-3% CPU idle). Rancher Desktop’s k3s alone uses ~500MB idle.
  • Full control: you manage Docker like a Linux server β€” systemd, daemon.json, config files. No GUI abstraction layer hiding what is happening.
  • Native Docker compatibility: 100% compatible socket, Compose, Swarm, BuildKit. No emulation layer, no SSH proxy, no podman-compose edge cases.
  • Familiarity: if your production servers run Docker Engine on Linux, your dev environment matches.

When this approach is best Link to this section

  • You are CLI-first and prefer terminal workflows over GUIs.
  • Your company needs to avoid Docker Desktop licensing costs.
  • You use Docker Compose extensively, including network_mode: host or port mappings below 1024.
  • You rely on Docker Swarm.
  • You want minimal resource overhead on your dev machine.
  • You already work inside WSL for development and want the container daemon in the same distro.

When to pick something else Link to this section

See the full comparison later in this post, but in short:

  • Rancher Desktop if you need a GUI and one-click Kubernetes.
  • Podman Desktop if your security policy requires rootless containers.
  • Docker Desktop if you need native Windows containers or already have a license.

Installing Docker Engine on WSL Link to this section

To install Docker Engine in WSL, first follow the official docs for your distro. I use Ubuntu, so I’ll give instructions for it in this post. For other distros, click here.

We need to add the docker repository to apt’s sources and install its components (Docker Engine, Docker CLI, Containerd, BuildX and Docker Compose):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

To use the docker CLI command without root permissions, create a docker group and add your user to it:

1
2
3
sudo groupadd -f docker
sudo usermod -aG docker $USER
newgrp docker

The newgrp docker command starts a new shell with the group membership applied. Log out and back in for a permanent effect.

Then start the Docker service:

1
sudo service docker start

If you enabled systemd in WSL (see below), use sudo systemctl start docker instead.

And test with the hello-world image:

docker run hello-world

How to auto-start Docker when WSL starts Link to this section

There are two approaches:

WSL 0.67.6+ supports systemd β€” Docker starts automatically as a systemd service.

In your WSL distro, edit /etc/wsl.conf (create it if it doesn’t exist) and add:

1
2
[boot]
systemd=true

Then, from Windows (PowerShell or CMD), shut down WSL:

1
wsl --shutdown

Reopen your WSL shell and run docker ps to confirm Docker is running.

Note: systemd adds a small startup delay (~14 seconds on my machine). If this is a concern, use the profile script approach below.

With a profile script (alternative, no systemd) Link to this section

If systemd is not available or you want to avoid its startup latency, add the following script to your shell profile (.zshrc, .bashrc, etc.):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if grep -qi "microsoft" /proc/version > /dev/null 2>&1; then
    if service docker status 2>&1 | grep -q "is not running"; then
        # Use wsl.exe to run as root without password prompt.
        # Falls back to sudo if wsl.exe is unavailable.
        if command -v wsl.exe &> /dev/null; then
            wsl.exe --distribution "${WSL_DISTRO_NAME}" --user root \
                --exec /usr/sbin/service docker start > /dev/null 2>&1
        else
            sudo service docker start
        fi
    fi
fi

This checks if you’re inside WSL (in case you share your profile across machines) and starts Docker if it’s not running.

Source: https://github.com/nickjj/dotfiles/commit/badd3265e5c8f6eca90d3b57df29292545332500

How to access Docker from Windows Link to this section

To access the Docker socket from outside WSL, configure the Docker daemon to listen on a TCP port.

If using systemd Link to this section

Do NOT edit /lib/systemd/system/docker.service directly β€” changes are lost on package updates. Instead, create a systemd override:

1
sudo systemctl edit docker.service

Add the following, which clears the default ExecStart and adds a TCP listener:

1
2
3
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375 --containerd=/run/containerd/containerd.sock

Then reload systemd and restart Docker:

1
2
sudo systemctl daemon-reload
sudo systemctl restart docker

Security note: tcp://0.0.0.0:2375 exposes the Docker API without authentication on all interfaces. WSL2 uses a NAT network, which limits exposure, but consider using a firewall or TLS if you are on an untrusted network.

If starting with a profile script (no systemd) Link to this section

Edit /etc/docker/daemon.json and add the hosts configuration:

1
2
3
{
  "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]
}

Then restart Docker:

1
sudo service docker restart

When you set hosts in daemon.json, the default Unix socket is not used unless you explicitly include it (as shown above).

After either setup, configure the DOCKER_HOST environment variable on Windows:

1
DOCKER_HOST=tcp://localhost:2375

(or tcp://[::1]:2375 for IPv6).

With DOCKER_HOST set, tools like Testcontainers and Localstack will use the Docker engine running inside WSL.

πŸ’‘ I wrote posts about Testcontainers and Localstack.

Access from the command line Link to this section

I prefer running commands from the WSL shell, but there are alternatives:

  • Install Docker CLI on Windows (it respects DOCKER_HOST).
  • Run commands via wsl: wsl -d Ubuntu docker ps

Comparison with alternatives Link to this section

This section compares Docker Engine in WSL with Rancher Desktop and Podman Desktop β€” the two main free alternatives that also avoid Docker Desktop’s license cost.

Rancher Desktop Link to this section

Docker Engine in WSLRancher Desktop
Apache 2.0, free for all org sizesApache 2.0, free for all org sizes
No GUI β€” CLI-onlyGUI dashboard for containers, images, volumes
No Kubernetes bundled (DIY with kind/k3d/minikube)Bundled k3s with version dropdown
Lower resource usage (~14s startup with systemd, ~100MB daemon)Higher resource usage (k3s idle ~500MB RAM, ~3.8GB disk base)
Manual config (systemd, daemon.json, profiles)GUI-driven config, abstraction layer over dockerd/nerdctl
Native Docker socket, Compose, Swarm β€” 100% compatibleDocker-compatible via dockerd or nerdctl

Rancher Desktop is the closest like-for-like Docker Desktop replacement. It is the right choice if you want a GUI and one-click Kubernetes without paying for Docker Desktop. The trade-off is higher resource usage and less control over the daemon β€” you manage Docker through Rancher’s GUI rather than directly.

Podman Desktop Link to this section

Docker Engine in WSLPodman Desktop
Daemon-based (dockerd runs as root)Daemonless, rootless by default β€” containers run as your user
Docker Compose 100% compatiblepodman-compose has gaps: network_mode: host behaves differently in rootless mode, bind-mount path translation can break on WSL (resolves to D:\ paths instead of WSL paths)
Docker Swarm supportedDocker Swarm not supported
BuildKit native β€” warm builds ~6sBuildah-based β€” warm builds ~8s, layer caching differences
Ports < 1024 (-p 80:80) work normallyPorts < 1024 require extra capabilities in rootless mode
Daemon always running (background CPU ~1-3%)No persistent daemon β€” zero resource usage when idle

Podman’s daemonless, rootless architecture is a genuine security improvement β€” a container breakout gives the attacker your user permissions, not root. However, the compatibility friction with Docker tooling is real. Most workflows work with alias docker=podman, but edge cases surface in Docker Compose, port mapping, and tools that hardcode /var/run/docker.sock.

Choose Podman if security requirements mandate rootless containers or your team is aligned with Red Hat / OpenShift tooling. Choose Docker Engine in WSL if you want everything to “just work” without configuration workarounds.

Possible errors Link to this section

Invalid credsStore configuration Link to this section

If Docker Desktop was previously installed, the credentials store may be set to docker-credential-desktop, causing this error:

1
error getting credentials - err: exec: "docker-credential-desktop": executable file not found in $PATH, out: ``

Fix: edit ~/.docker/config.json and remove the credsStore property.

Duplicate hosts configuration Link to this section

If you set up Docker with systemd and get the following error on startup, it is likely due to duplicate host configuration:

1
2
3
4
5
6
7
8
> sudo service docker restart
Job for docker.service failed because the control process exited with error code.
See "systemctl status docker.service" and "journalctl -xeu docker.service" for details.

> systemctl status docker.service
Aug 29 06:18:04 DanielPC systemd[1]: docker.service: Main process exited, code=exited, status=1/FAILURE
Aug 29 06:18:04 DanielPC systemd[1]: docker.service: Failed with result 'exit-code'.
Aug 29 06:18:04 DanielPC systemd[1]: Failed to start Docker Application Container Engine.

The systemd service file passes hosts via -H flags, but you also set hosts in /etc/docker/daemon.json. Docker rejects the duplicate configuration.

To fix: remove the hosts key from /etc/docker/daemon.json and configure the TCP listener via the systemd override as shown above.

What is 9P and why does it matter? Link to this section

9P (Plan 9 Filesystem Protocol) is a network filesystem protocol from Bell Labs. WSL2 uses it to share Windows drives (NTFS) into the Linux VM. When you access C:\ from inside WSL at /mnt/c, every file operation travels over 9P:

1
Windows (NTFS)  ←→  9P protocol over VM socket  ←→  WSL2 Linux VM (/mnt/c)

Every stat(), readdir(), open(), close() serializes and round-trips across the VM boundary. For workloads touching thousands of small files β€” npm install, git status, find, Vite/webpack file watchers β€” this compounds into a ~10Γ— slowdown compared to native ext4 inside WSL.

Inside ~/ (the WSL home directory), files live on an ext4 filesystem in a VHDX file β€” the same kernel, same driver, no protocol translation overhead. The difference is not about Docker or WSL version; it is about whether the filesystem path crosses the 9P boundary.

The fix is always the same regardless of which container tool you use: store source code under ~/ inside WSL (not on /mnt/c/), bind-mount from Linux paths into containers, and access files from Windows tools via the \\wsl.localhost\Ubuntu\home\you\repo network path.

What about the \\wsl.localhost\ network share? Link to this section

Windows exposes every WSL distro’s filesystem as a hidden SMB share at \\wsl.localhost\Ubuntu\, \\wsl.localhost\Debian\, etc. You can even map it to a drive letter:

1
net use Z: \\wsl.localhost\Ubuntu

This is Windows accessing Linux files over the network β€” it goes through the Windows networking stack, not through 9P. This is the recommended way to edit WSL files from Windows applications (VS Code, Explorer, etc.) without crossing the slow 9P path.

Crucially: this does NOT affect Docker bind-mount performance. Docker inside WSL bind-mounts Linux paths using the Linux kernel. If your repo is at ~/my-project, you mount it as -v ~/my-project:/app β€” Linux ext4 to Linux overlayfs, no 9P involvement. The Windows SMB path is only for editing, not for container mounts.

Older blog posts might recommend the reverse β€” storing code on Windows and accessing it via /mnt/c β€” but that pattern is the slow path. Keep code in WSL, edit from Windows via the \\wsl.localhost\ share.

References Link to this section

πŸ’¬ Like or have something to add? Leave a comment below.
Ko-fi
GitHub Sponsor
Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy