A working WSL 2 Ubuntu development setup

Oli Zimpasser
10 min readOct 26, 2020

UPDATE: January, 2023

This article was written before Microsoft introduced systemd for WSL as written here. If available on your system, use the official way instead of genie.

Introduction

My development setup has two fundamental requirements: the availability of Unix shell scripting and corporate tools/compliance standards.

Furthermore I need a couple of tools and programs and while the required tools vary from project to project, I always want to install IntelliJ IDEA as my Java / JavaScript IDE, Visual Studio Code as my general purpose editor, docker for containers, Microsoft Teams for collaboration, KeePass as a password manager, a terminal application for bash/fish scripting, Meld as a visual diff tool, Postman for testing REST APIs and an assorted choice of browsers, because — you know — browsers are the thing nowadays.

So how can I get that with Windows as the host platform?

The issue with my setup for the last 12 month

I was working with Ubuntu as my development system for the last 12 month and while I am pretty happy with the setup in general, I had to run it inside a VirtualBox VM to align with corporate compliance regulations.

That again works quite well but there is one serious drawback: memory allocation between the host system and the virtual machine. VirtualBox, as well as VMware, require you to define the available — and the allocated — memory of the virtual machine before you start it.

In situations where you have plenty of memory on the host system and your needs inside the virtual machine are limited, this is no issue at all. Unfortunately my use-case requires a lot of memory inside the VM and on top of that some flexibility for the host system — and that makes the whole setup some sort of a problem.

Originally I wanted to solely use the virtual machine and so I max’ed out the available memory to the guest operating system.

My laptop has 32 GB of RAM and I assigned 24 GB of RAM to the Ubuntu VM. As said the original idea was to start all applications inside the VM to avoid any switching between the host and the VM, but there are things one cannot do (easily) inside a VM and that is for example video conferencing. We use Microsoft Teams which does exist as a (somewhat) native Linux application, but as VirtualBox does not support the camera — at least without commercial addons — I needed to start and use Teams on Windows.

A possible solution: WSL 2

Microsoft has built a decent native Hypervisor into Windows and with version 2 of WSL (Windows Subsystem for Linux) it supports memory reclaim mechanisms, thus Windows and Ubuntu can increase and decrease their memory distribution at runtime. This feature in combination with Docker Desktop for Windows could make a Windows-HyperV-Ubuntu-X11 setup not only a reality, but it can be superior to a VirtualBox solution.

The Windows installation

For the sake of reproducibility I have written this article in an tutorial-like style. So if you want to follow along, start with installing Visual Studio Code on Windows.

The first step is to install WSL 2 on Windows as described here and as we will choose Ubuntu 20.04 as the Linux distribution in a later step, skip step 7. For now you should also install Windows Terminal.

The 2nd step is to install VcXsrv a X11 server for Windows. We will start and configure it later.

The 3rd is to create an Ubuntu VM. So you should download the Ubuntu WSL image from Microsoft Store.

The 4th step on the Windows 10 installation is Docker Desktop for Windows. Just start it after the installation. If the default WSL distro is Ubuntu you don’t need to change any settings in Docker. Check the default distro (the asterisk marks the default):

If you have or want to have a different default distro, you have to add WSL integration for Ubuntu in the Docker settings dialog under Resources / WSL integration:

The final step is to start Ubuntu via the Start Menu. After initializing the Ubuntu VM in WSL 2 and creating of a user, start with installing the latest Ubuntu updates and switch to the fish shell.

Open the Windows Terminal, then open a “Ubuntu shell” (the little downwards arrow in the menu) and type:

sudo apt update && sudo apt upgrade -ysudo apt install -y fishchsh -s /usr/bin/fish

At this point I would like to point out two arguable decisions I made:

  • all my configurations assume fish as the default shell
  • I will use remote X11 forwarding to show GUI Linux applications, but you could do that via VNC or RDP as well — it’s just that I think X11 creates a more homogeneous experience and it completely eradicates multi-monitor issues

Starting a Ubuntu VM with WSL 2 is super simple, but for a real-world usage, there are a couple of issue to overcome.

Setting up: Visual Studio Code

Inside the Ubuntu shell type

code

as you see WSL is installing Visual Studio Code in Ubuntu. From now on you can start the Windows Visual Studio Code for editing files on the Linux file system. You might want to read this for a deeper understanding.

Issue 1: No systemd

The first issue you will find with WSL is that it doesn’t come with systemd, but many things require systemd, so people created genie.

To install genie und you need a .NET runtime. So we start with installing this:

wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.debsudo dpkg -i packages-microsoft-prod.debsudo apt updatesudo apt install -y dotnet-runtime-3.1

After this you can install genie:

echo "deb [trusted=yes] https://wsl-translinux.arkane-systems.net/apt/ /" | sudo tee /etc/apt/sources.list.d/wsl-translinux.list > /dev/nullsudo apt updatesudo apt install -y systemd-genie

Now genie — aka systemd — can be started with genie -s. But before we do this let’s look into a couple of other issues.

Issue 2: Changing PATH

Creating a genie shell (via genie -s) changes the PATH.

echo $PATH
genie -s
echo $PATH

This is a problem, as the docker and WSL tools (like code) disappeared from the path. Another problem is, that the switch to fish removed the path to /snap/bin.

My (probably too) simple solution is to set the PATH in the startup script. So type:

code ~/.config/fish/config.fish

and put the following line into the file (keep in mind to replace “oglimmer” with your Windows user name):

set PATH "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/mnt/c/WINDOWS/system32:/mnt/c/WINDOWS:/mnt/c/WINDOWS/System32/Wbem:/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/:/mnt/c/WINDOWS/System32/OpenSSH/:/mnt/c/Program Files/dotnet/:/mnt/c/Program Files/Docker/Docker/resources/bin:/mnt/c/ProgramData/DockerDesktop/version-bin:/mnt/c/Users/oglimmer/AppData/Local/Microsoft/WindowsApps:/mnt/c/Users/oglimmer/AppData/Local/Programs/Microsoft VS Code/bin:/snap/bin"

Issue 3: Deleting all content of /tmp

The next issue comes with the temp directory. Start a new Windows Terminal tab for Ubuntu and type:

(To reproduce this, you might need to “wsl --shutdown” first from PowerShell)

ll /tmp
genie -s
ll /tmp

When genie starts, it somehow deletes everything inside /tmp directory. This is another problem as some programs use /tmp for their state files. An example is ssh-agent which puts its unix socket file into /tmp.

So we need to take that into consideration when starting programs.

Issue 4: Changing process tree

The next issue comes with the process tree. Start again a new Windows Terminal tab for Ubuntu and type:

(To reproduce this, you might need to “wsl --shutdown” first from PowerShell)

ps -efH
genie -s
ps -efH

As you see getting into the “systemd aware shell” changes the process tree. Our shell is no longer coming from 3 /init processes, it now has a runuser parent. This has implications on scripts checking for processes. Again we need to keep that in mind.

Issue 5: Changing host IP for DISPLAY

To start GUI based applications on the remote Windows 10 X Server, the DISPLAY variable has to be defined properly.

Fortunately this is no real issue and we simply put this

set -x DISPLAY (cat /etc/resolv.conf | grep nameserver | cut -d ' ' -f 2):0

into ~/.config/fish/config.fish (via code).

Ubuntu GUI preparation

The next step is to install the Ubuntu desktop packages. We do this via tasksel:

sudo apt install -y taskselsudo tasksel install ubuntu-desktop

Starting VcXsrv in Windows 10

Before we can use any GUI programs in Ubuntu, we have to start the X Server in Windows.

Start VcXsrv via “XLaunch” and confirm the first dialog with next:

Confirm the second dialog with next:

Change “native opengl” to false and “Disable access contrl” to true. Then confirm with next and finally click finsh.

You should now see an X icon in the Windows icon area on the lower right corner. You might need to accept a Firewall change for the “public network”.

The manual daily startup routine :(

We talk about about the Good and the Bad. Now to the Ugly.

Taking the issues from above into account I have to do a couple of manual steps after each start. After opening Windows Terminal and selecting the Ubuntu tab I have to type:

genie -seval (ssh-agent -c); set -Ux SSH_AGENT_PID $SSH_AGENT_PID; set -Ux SSH_AUTH_SOCK $SSH_AUTH_SOCKssh-add ~/.ssh/id_rsagnome-terminal

If gnome-terminal exits with an error and need to execute a “wsl --shutdown” from PowerShell. Then start the “daily startup routine” again.

At this point I minimize the Windows Terminal and switch to the newly created gnome terminal. The reason for this is that I prefer the select and click behavior of the gnome-terminal, especially with features like X Window selections.

Docker and Client Certificates

If your company has its own docker registry and authentication is done via client certificates (also called mutual TLS) you need to add a client.key and client.cert (both PEM encoded) into

C:\Users\<username>\.docker\certs.d\docker.mycompany.com

Unfortunately this is not enough and after each restart of Docker’s Desktop for Windows application you need to run this command from PowerShell:

docker run --rm --privileged -d -v /:/host -v $env:UserProfile\.docker\certs.d:/certs.d alpine cp -r /certs.d /host/etc/docker/certs.d

IntelliJ IDEA

Install it via:

sudo snap install intellij-idea-ultimate --classic

Start it from the Ubuntu tab in Windows terminal via:

intellij-idea-ultimate &

Miscellaneous things to mention

  • While memory allocation is flexible, you can limit the max CPU and memory for WSL. This can be configured via .wslconfig
  • To have the Windows Terminal as similar as possible to the fish shell, I want to have an alias for “ll” in PowerShell as well:
cd $env:USERPROFILE\Documentsmd WindowsPowerShell -ErrorAction SilentlyContinuecd WindowsPowerShellNew-Item Microsoft.PowerShell_profile.ps1 -ItemType "file" -ErrorAction SilentlyContinueecho "Set-Alias ll ls" > Microsoft.PowerShell_profile.ps1Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
  • Selecting a text in PowerShell should select full paths, so open the Settings and add:
"wordDelimiters": " \\()\"'-:,;<>~!@#$%^&*|+=[]{}~?│",
  • Using FanzyZone in PowerToys enables you do quickly drop and resize windows into different zones. Very handy for ultra-wide monitors.
  • You might want to get familiar with the “wsl” command line tool in PowerShell. This can be used to restart, import or export the Ubuntu VM.

Running programs in Linux vs. Windows

The setup enables me to run inside Ubuntu:

  • IntelliJ IDEA
  • gnome-terminal
  • meld
  • bash/fish scripts
  • Firefox, Chrome, Opera

While I can run natively under Windows:

  • Visual Studio Code
  • Firefox, Chrome, Opera, Edge
  • Teams
  • KeePass
  • Windows Terminal
  • Postman

All programs look and behave the same. They all have it’s own box in the Windows taskbar, can be moved to different monitors and share the Windows Clipboard.

File system access

You can easily access the Windows file system from Ubuntu via /mnt/c/.

Accessing the Ubuntu file system from Windows works as easy as \\wsl$\Ubuntu\

Docker

You can use docker from Windows and Ubuntu as both share the same docker daemon in the background. Also docker-compose is available by default.

Known issues

  • When docker has strange network issues reboot Windows 10
  • Sometimes the Windows 10 X server cannot be reached (e.g. when you want to start gnome-terminal) and you need to shutdown the WSL instances
  • Sometimes the docker process holds a handle on directories. This results in a file/directory cannot be written/read within WSL with the error message “file a resource busy”. In those cases quit Docker, then delete the directory.
  • If starting gnome-terminal doesn’t work inside a genie bottle, unset DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR, still I am unable to create new tabs via “ -- tab” while inside a bottle.
  • If starting gnome-terminal brings an error “Couldn’t register with accessibility bus” set NO_AT_BRIDGE to 1.

Don’ts

  • Do not install Visual Studio Code inside the Ubuntu VM. It has to be used from /mnt/c/Users/oglimmer/AppData/Local/Programs/Microsoft VS Code/bin
  • Do not install docker, docker.io or docker-compose inside the Ubuntu VM. It has to be used from /mnt/c/Program Files/Docker/Docker/resources /bin or /mnt/c/ProgramData/ DockerDesktop/version-bin

Conclusion

A dream came true and I got the best of both worlds: the development tools from Linux and the corporate tools from Windows — everything working seamlessly together.

Pros:

  • it mostly feels like one system — and not as two separated operating systems
  • you can start Linux GUI applications and they are shown in Windows as a regular window
  • the setup is stable and never crashed for me
  • memory is handled automatically and without my interaction or attention

Cons:

  • The docker issue around “file and resource is busy” is annoying and forces you into occasional restarts of Docker
  • Using remote X11 makes the regular Ubuntu Desktop not available (at least I haven’t figured it out)
  • Using “genie” to start and use systemd creates many problems you usually don’t have to deal with

--

--