Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GUI-Enabled C++ Dev Container for WSL2 + Docker Desktop (Windows)

Run a fully containerized C++ development environment on Windows — via WSL2 and Docker Desktop — where GUI windows opened inside the container (image viewers, 3D plots, debug overlays) show up on your Windows desktop, properly authenticated and firewall-scoped, without ever disabling X11 security.

If you've tried to get cv::imshow(), a Pangolin trajectory viewer, or any other Linux GUI tool working from inside a Docker devcontainer on Windows and hit a wall of "Can't open display" errors — this repo is the result of debugging that wall end to end, documented so you don't have to repeat the same dead ends.

Pangolin trajectory viewer running inside the devcontainer

Table of contents


Why this exists

WSL2 ships with WSLg, which gives native Linux GUI apps running directly in a WSL terminal seamless windows on your Windows desktop, no extra setup. The natural assumption is that a Docker container running "in WSL2" gets this for free too.

It doesn't — at least not with Docker Desktop.

Docker Desktop runs containers through its own internal system distro (visible as docker-desktop / docker-desktop-data in wsl -l -v), which is separate from your actual interactive WSL distro (e.g. Ubuntu-22.04). WSLg's socket-sharing magic is tied to your real distro and does not reliably bridge into Docker Desktop's internal one. This is a known, still-open limitation — see docker/for-win#14403 and devcontainers/discussions#173.

Most existing guides solve this by running an X server on Windows (commonly VcXsrv) and telling the container to connect to it — but almost all of them do so with "Disable access control" checked, which means any process on your machine or local network can connect to your display: read keystrokes across all your windows, take screenshots, inject fake input. That's a real, well-documented weakness of the X11 protocol, not a hypothetical.

This repo does the same thing (X server on Windows) but with:

  1. X11 authentication via a rotating secret cookie (xauth) instead of disabling access control, and
  2. A Windows Firewall rule scoped to only your own machine's internal WSL/Docker networks, so nothing external can even attempt a connection.

What this actually enables

Once set up, any GUI code inside your container just works, the same as it would on a native Linux desktop:

  • OpenCV image/video debuggingcv::imshow("frame", img); cv::waitKey(0); pops a real window on your desktop.
  • 3D visualization for robotics / computer vision / SLAM — libraries like Pangolin (widely used for plotting camera trajectories, point clouds, and pose graphs) render correctly, so you can visually debug a pipeline instead of squinting at raw numbers in a terminal.
  • GDB with any GUI front-end, or any tool that opens secondary windows.
  • Reproducible onboarding — a teammate can clone this repo, open it in VS Code, and get the identical environment and identical GUI capability, without installing OpenCV/CMake/etc. on their actual Windows machine at all.

How it works (architecture)

 ┌─────────────────────────┐        TCP :6000        ┌───────────────────────┐
 │   Docker container       │  (auth cookie required) │   VcXsrv (X server)   │
 │   (your C++/OpenCV code) │ ───────────────────────▶│   running on Windows  │
 │   DISPLAY=<docker-ip>:0  │                          │   -auth <cookie file> │
 └─────────────────────────┘                          └───────────┬───────────┘
                                                                    │
                                                          draws actual window
                                                                    │
                                                                    ▼
                                                          your Windows desktop

Two things gate every connection attempt before a window can ever appear:

  1. Windows Firewall — only allows inbound TCP:6000 traffic from your own machine's WSL/Docker internal subnets. Anything else is dropped before it even reaches VcXsrv.
  2. X11 auth cookie (xauth) — even a connection from an allowed subnet is rejected unless it presents the exact matching secret, regenerated on every launch.

Prerequisites

  • Windows 10/11 with WSL2 installed and a Linux distro set up (e.g. Ubuntu)
  • Docker Desktop, WSL2 backend enabled
  • VS Code with the Dev Containers extension
  • VcXsrv — free, from https://sourceforge.net/projects/vcxsrv/ (install with default options; don't let it auto-launch after install)

Setup

1. Find your WSL distro's exact name, and make it the default

wsl -l -v

Note the exact name (e.g. Ubuntu-22.04) — case and formatting matter for the scripts later.

wsl --set-default <YourDistroName>

This avoids a common and confusing failure mode where WSL commands silently target Docker Desktop's own internal distro instead of yours (see Design decisions & dead ends).

2. Find your WSL gateway IP and your Docker-internal IP

Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.InterfaceAlias -like "*WSL*" }

Note the IPAddress (e.g. 172.19.64.1) and its PrefixLength (e.g. 20).

Then, from inside any running container (a throwaway docker run -it ubuntu bash works fine if you don't have one handy yet):

getent ahostsv4 host.docker.internal

Note this IP too — it's commonly 192.168.65.254 on Docker Desktop, but confirm it on your machine, don't assume.

3. Place the scripts

Clone/copy this repo. In WSL, make sure generate-cookie.sh is executable and reachable at ~/generate-cookie.sh (or update the path referenced inside start-vcxsrv.ps1 if you place it elsewhere):

cp scripts/generate-cookie.sh ~/generate-cookie.sh
chmod +x ~/generate-cookie.sh

Copy scripts/start-vcxsrv.ps1 to a convenient Windows path, e.g. C:\Users\<you>\gui-cpp-devcontainer\scripts\start-vcxsrv.ps1.

4. Fill in the placeholders

In scripts/start-vcxsrv.ps1, edit the top three variables:

$WinUser        = "yourname"          # your actual Windows username
$WslDistro      = "Ubuntu-22.04"      # exact name from `wsl -l -v`
$DockerInternal = "192.168.65.254"    # from step 2

In .devcontainer/devcontainer.json, replace every YOUR_WINDOWS_USERNAME and REPLACE_WITH_YOUR_DOCKER_INTERNAL_IP placeholder with your real values.

5. Scope the firewall (one-time, admin PowerShell)

Using your WSL subnet from step 2 (e.g. 172.19.64.0/20), open an elevated PowerShell ("Run as administrator") and run:

New-NetFirewallRule -DisplayName "VcXsrv WSL only" `
    -Direction Inbound -Program "C:\Program Files\VcXsrv\vcxsrv.exe" `
    -Protocol TCP -LocalPort 6000 `
    -RemoteAddress <your_wsl_subnet>,192.168.65.0/24 `
    -Action Allow

This is the core security step — only your own machine's internal WSL/Docker networks can ever reach the X server.

From this point on, never launch VcXsrv manually from an elevated window. Always let the script start it — see Design decisions & dead ends for why this matters.


Day-to-day usage

  1. Open the project folder in VS Code.
  2. Click "Reopen in Container".
  3. That's it — VS Code silently runs start-vcxsrv.ps1 for you before the container even starts, regenerating the cookie and (re)launching VcXsrv.

Open a terminal inside the container and test:

xeyes

A little window with eyes that follow your mouse should appear on your Windows desktop. If it does, any real GUI code will work the same way.


Verifying it's actually secure

Don't just take the setup's word for it — prove it to yourself.

1. Connections without the cookie should fail:

mv /root/.Xauthority /root/.Xauthority.bak
xeyes

Expect an error (Authorization required...), not a working window.

2. Restore it and confirm it works again:

mv /root/.Xauthority.bak /root/.Xauthority
xeyes

3. Confirm the firewall rule is actually scoped:

Get-NetFirewallRule -DisplayName "VcXsrv WSL only" | Get-NetFirewallAddressFilter

RemoteAddress should list your specific subnets — never Any.


Troubleshooting

Symptom Root cause Fix
Error: Can't open display: :0 Trying to use WSLg's native /mnt/wslg socket path, which Docker Desktop containers can't reliably reach Use the VcXsrv + TCP approach in this repo instead of raw WSLg passthrough
ls: cannot access '/mnt/wslg/.X11-unix/' inside container Docker Desktop's internal distro mounts WSLg content at /mnt/host/wslg, not /mnt/wslg Not needed with this repo's approach — mentioned here in case you're debugging a different, WSLg-socket-based setup
Authorization required, but no authorization protocol specified XAUTHORITY file is missing, empty, or mounted as an empty directory instead of a file Check ls -la /root/.Xauthority inside the container — it must show as a regular file, not a directory. Mount from a plain Windows path (C:\Users\...), not directly from a WSL distro path — the latter can silently resolve to an empty directory under Docker Desktop
xauth: (argv): bad "add" command line Cookie generation produced an empty string, usually because xxd isn't installed on the distro This repo uses openssl rand -hex 16 instead, which is far more reliably present
Cannot establish any listening sockets - Make sure an X server isn't already running (VcXsrv popup) A previous VcXsrv process is still holding port 6000 Task Manager → Details tab → end every vcxsrv.exe process, confirm any admin prompts, then retry
Stop-Process: Accès refusé / Access denied when killing vcxsrv A previous VcXsrv instance was started from an elevated PowerShell window; a non-elevated script can't terminate it Kill it manually once via Task Manager (elevated), then never launch VcXsrv from an elevated window again
<3>WSL ... ERROR: getpwuid(0) failed when running wsl.exe commands wsl.exe without -d <name> targets whatever your default distro is — which can silently be Docker Desktop's own internal docker-desktop distro instead of yours Always pass -d <YourDistroName> explicitly, and/or run wsl --set-default <YourDistroName>
cat: /some/mounted/file: Is a directory Docker created an empty directory at the mount target instead of mounting the actual file — happens with certain WSL-distro-path mount sources under Docker Desktop Mount from a plain Windows filesystem path instead
Connects but times out / generic "Can't open display" with no auth-specific detail Container genuinely cannot route to the IP you gave it — using the raw WSL gateway IP from inside a container doesn't work, since containers and WSL sit on different internal networks Use getent ahostsv4 host.docker.internal from inside the container to find the correct address, and widen the firewall rule to cover Docker Desktop's internal subnet (commonly 192.168.65.0/24) too
host.docker.internal resolves to an IPv6 address Some systems return IPv6 by default via plain getent hosts Use getent ahostsv4 host.docker.internal specifically to force an IPv4 result, and use that IPv4 address everywhere
xeyes appears to hang with no prompt returning This is often success, not failure — xeyes doesn't return to the prompt while its window is open Alt-Tab and check other monitors before assuming it failed
New-NetFirewallRule: Accès refusé PowerShell window isn't running as Administrator Right-click PowerShell → "Run as administrator", retry
Get-NetIPAddress -InterfaceAlias "vEthernet (WSL)" returns nothing The WSL adapter's exact name varies by Windows/WSL version (vEthernet (WSL), vEthernet (WSL (Hyper-V firewall)), etc.) Use a wildcard match instead: Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.InterfaceAlias -like "*WSL*" }
Very slow apt-get install for libopencv-dev (5-10+ minutes) Normal — OpenCV pulls in a large dependency tree, and unpacking many small files is slow through Docker Desktop's WSL2-backed overlay filesystem Let it finish once; it's cached for all future rebuilds. If abnormally slow, exclude your WSL .vhdx from Windows Defender real-time scanning
Corporate VPN interferes with WSL DNS / connectivity VPN software commonly overrides WSL's /etc/resolv.conf, breaking IP-detection tricks that rely on it Don't rely on resolv.conf for the WSL gateway IP — this repo uses Get-NetIPAddress on the Windows side instead, which is unaffected

Design decisions & dead ends

A few notes on why the setup looks the way it does, for anyone comparing this against other guides or wondering "why not the simpler way":

  • Why not just mount /mnt/wslg into the container? This is the first thing most people try, since it's how WSLg itself works for plain WSL terminals. It doesn't work reliably with Docker Desktop because containers run through Docker Desktop's separate internal distro, which only partially mirrors /mnt/wslg's contents (notably missing .X11-unix) or exposes it at a different path (/mnt/host/wslg) depending on version. This repo bypasses the whole problem by using a TCP-based X server instead.
  • Why VcXsrv and not another X server? It's free, well-established, and supports the -auth flag needed for proper cookie-based authentication (unlike some lighter-weight alternatives).
  • Why regenerate the cookie on every launch, instead of once? It costs nothing (sub-second) and means a stale/leaked cookie from a previous session is never still valid.
  • Why two IP addresses registered in the cookie file, not one? Depending on exact Docker Desktop networking behavior, a container may present as connecting from either the raw WSL gateway IP or Docker's internal host IP. Registering both avoids "works on my machine but not yours" style inconsistency.
  • Why not just switch Docker Desktop to the native WSL2 engine (docker context use default) instead of all this? That is a valid alternative that sidesteps the problem differently, since containers would then run inside your actual WSL distro and could access /mnt/wslg directly. It wasn't used here because it requires installing Docker Engine natively inside WSL and carefully managing two potentially-conflicting Docker installations, which is a bigger and riskier change for most people already relying on Docker Desktop for other projects. If you're setting up a machine from scratch and don't mind that trade-off, it's worth investigating as a cleaner long-term alternative.

FAQ

Does this expose my display to my local network / the internet? No. The firewall rule restricts inbound connections to your own machine's internal WSL and Docker Desktop virtual networks only — these aren't routable from your LAN or the internet. Combined with cookie authentication, an attacker would need code execution on your own machine already to exploit this, at which point they'd have easier avenues anyway.

What happens if my WSL gateway IP changes after a Windows update? xeyes (or any GUI app) will stop connecting. Re-run the IP detection command from Setup step 2, update the firewall rule and devcontainer.json's DISPLAY value if the Docker-internal IP also changed, and rebuild the devcontainer. This is uncommon but not impossible.

Can I use this with a different base image / different libraries? Yes — the x11-apps and xauth packages and the devcontainer.json / script setup are independent of what else you install. Swap libopencv-dev / libeigen3-dev for whatever your project needs.

Do I need to keep VcXsrv running all the time? No. It only needs to be running while you're working in the devcontainer, and initializeCommand starts it automatically each time you open the project — no background service, nothing running at Windows startup.


License

MIT. Use, modify, and share freely.

About

Secure GUI passthrough for C++ dev containers on WSL2 + Docker Desktop. Run cv::imshow, Pangolin, or any X11 app in a devcontainer without disabling X11 access control.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages