Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

Native VS Code Dev Containers in WSL

This guide provides a complete solution for running VS Code Dev Containers in WSL natively, using WSL Containers (wslc.exe) from within a WSL environment, avoiding the need for Docker Desktop.

For background on the WSL Containers project, see the official release blog post.

What this does for the user

Running Dev Containers inside WSL natively has historically presented severe roadblocks. VS Code's Dev Containers extension runs inside Windows, connects to WSL via a Node.js interop layer, and issues Docker commands inside your WSL shell to orchestrate the containers.

Without this solution, users experience:

  • Image build failures due to unsupported --platform arguments injected by VS Code.
  • Containers that fail to mount directories correctly because Windows UNC paths are mangled.
  • Infinite hangs during the "Starting container" phase because keep-alive pings get trapped in interop buffers.
  • Silent container crashes when interop pipes cross the Windows-Linux boundary incorrectly.

This solution completely mitigates these bugs, allowing you to:

  • Type devc in any WSL project folder containing a .devcontainer to instantly boot it.
  • Avoid running a separate heavy Docker daemon inside WSL.
  • Share the same fast Docker runtime between your Windows host and WSL containers effortlessly.

How to use it

1. Install the Wrapper Script (wslc_wrapper.sh)

The wrapper script sits between VS Code and WSL Containers (wslc.exe), intelligently translating paths and mitigating interop crashes.

  1. Save wslc_wrapper.sh to a directory in your WSL $PATH (e.g., ~/.local/bin/wslc).
  2. Make it executable:
    chmod +x ~/.local/bin/wslc
  3. Ensure that ~/.local/bin comes before /mnt/c/.../wslc.exe in your $PATH so the wrapper is prioritized.

(Note: VS Code Dev Containers looks for docker or a specified executable path in its settings. Ensure your VS Code Dev Containers extension is configured to use wslc as its Docker executable path).

2. Add the Launcher Function (devc)

The devc bash function lets you bypass the VS Code UI and natively boot into a Dev Container context right from your WSL terminal.

  1. Open your ~/.bashrc (or ~/.zshrc).
  2. Paste the contents of devc_launcher.sh at the bottom of the file.
  3. Reload your terminal: source ~/.bashrc.

3. Usage

Navigate to any project with a .devcontainer configuration and simply type:

devc

VS Code will immediately open, successfully build the container, inject the proper path formats, bypass the interop bugs, and connect you perfectly.

4. Troubleshooting

The wrapper patches undocumented VS Code behaviors, so a Dev Containers extension update can break it (typically as an infinite hang or a failed build). To see exactly what VS Code is sending, enable the wrapper's trace log:

export WSLC_WRAPPER_LOG=/tmp/wslc_wrapper.log

Each invocation appends its raw arguments, processed arguments, the execution branch taken, and the exit code — usually enough to spot a changed flag or keepalive string. Unset the variable to disable logging.


Technical Deep Dive: Why it was complicated

Creating a seamless bridge between VS Code (Windows), WSL, and WSL Containers (wslc.exe) required untangling four overlapping technical bugs.

1. The Unsupported --platform Bug

VS Code Dev Containers recently began dynamically injecting the --platform linux/amd64 flag into Docker build processes depending on local system checks. However, the wslc.exe build command natively rejects this argument since it doesn't support it, throwing an ArgumentError that halts the entire container initialization sequence (whereas standard Docker Engine or Docker Desktop would typically accept it). Solution: The wrapper dynamically intercepts the build argument list and completely strips --platform (both the --platform <value> and --platform=<value> forms). If a future wslc.exe release adds multi-platform support, set WSLC_ALLOW_PLATFORM=1 in your environment to pass the flag through untouched.

2. The Path Translation Boundary

When VS Code initializes the Dev Container, it generates internal configuration labels (like devcontainer.config_file and devcontainer.local_folder) using absolute Linux paths (e.g., /home/user/workspace/app). Since wslc.exe is a Windows executable, it fails to resolve these paths and the volume mounts fail. Solution: The wrapper translates the Linux absolute paths in devcontainer.config_file/devcontainer.local_folder label values and in the host-path side of -v/--volume specs, using wslpath -w to produce correct Windows UNC paths (e.g., \\wsl.localhost\Ubuntu-24.04\home\...). Translation is context-aware — only the token following those flags is rewritten — so absolute paths in arguments destined for inside the container are never touched.

Additionally, wslc.exe rejects the --mount flag outright — but VS Code passes every mounts entry from devcontainer.json in --mount form. The wrapper converts these to equivalent flags wslc.exe accepts: type=bind/type=volume specs become -v source:target[:ro] (with the same path translation, and readonly mapped to :ro), type=tmpfs becomes --tmpfs target, and consistency hints (cached/delegated) are dropped. This is what makes devcontainer.json "mounts" — e.g. mapping agent config folders like ~/.claude and ~/.gemini into the container — work at all under wslc.

3. The 4KB WSL Interop Buffer Deadlock

This was the most insidious issue. During container startup, VS Code executes a run command containing a keepalive script that prints Container started and then idles using while sleep 1; do :; done. VS Code on Windows waits indefinitely to read the string Container started from the stdout pipe.

However, the WSL interop layer buffers output crossing from WSL to Windows up to 4KB (4096 bytes) to preserve performance. Since Container started is only 17 bytes, the buffer never flushed, trapping the signal inside the pipe. VS Code would hang waiting forever, eventually timing out. Solution: The wrapper detects the VS Code keepalive script and surgically injects a python padding command: printf '%4096s' ''. This instantaneously fills the 4KB buffer with empty spaces, forcefully flushing the pipe and delivering the Container started string to VS Code immediately.

4. The Node.js Interop Pipe Crash

After solving the buffer lock, a new bug emerged. If wslc.exe run executes and its stdout is wired directly to the pseudo-TTY pipe instantiated by VS Code's internal Node.js process across the WSL boundary, wslc.exe crashes silently. This is due to a known WSL bug where Windows executables fail to initialize their streams correctly if attached directly to an incompatible Linux pipe descriptor bridging the Windows host. Solution: For the run command specifically, the wrapper pipes the output through a native Linux command: wslc.exe ... | cat. This shields the Windows executable behind a standard Linux pipe, preventing the crash, while cat natively and safely transfers the buffer over the interop boundary back to VS Code. (However, we must not pipe short-lived interactive commands like exec, as piping them breaks standard IO streams needed for the VS Code handshake).


Filesystem Semantics: the Two-Hop Mount

The UNC path translation is not just a formatting workaround — it reflects how WSLC is actually built, and it has real consequences for what the container sees.

The architecture

WSLC containers do not run inside your WSL distro's VM. They run in a separate utility VM with its own kernel instance (verifiable: /proc/uptime differs between the distro and a container on the same machine). The only bridge between your distro's ext4 filesystem and the container VM is the Windows host, so a workspace mount travels:

distro ext4  →  Plan 9 server  →  Windows (\\wsl.localhost\...)  →  virtiofs share  →  container

Inside the container the mount appears as virtiofs, and POSIX metadata is synthesized at the hops rather than carried through. Practical consequences:

  • Every file appears as -rwxrwxrwx, owned by root:root. The execute bit is meaningless — set on everything.
  • chmod/chown inside the container are silent no-ops on mounted files.
  • Git sees phantom mode changes unless told otherwise (see mitigation below).
  • File-watching (inotify) across the mount is unreliable, and I/O is slower than a native filesystem.

Because the two VMs share no kernel or mount namespace, there is no Linux-path shortcut around this — the UNC translation and the double hop are unavoidable for host-directory mounts under the current WSLC architecture.

Mitigation: git fileMode

Add this to your devcontainer.json so repos don't show phantom mode diffs inside the container:

"postCreateCommand": "git config core.fileMode false"

Note this must be the repo-local setting: a repo cloned in WSL carries an explicit filemode = true in .git/config, which overrides any container-global config. The tradeoff is that .git/config is shared through the mount, so the WSL side also stops tracking mode changes for that repo. When you need to commit an execute bit from WSL, set it explicitly:

git update-index --chmod=+x somescript.sh

What Microsoft could do

The clean fix belongs in WSLC itself: wslc.exe could detect that a mount source is a \\wsl.localhost\<distro>\... UNC path and short-circuit the two-hop mapping — sharing the underlying distro path directly into the container VM (a metadata-preserving export from the distro to the container VM) instead of round-tripping through the Windows host's Plan 9 client. The UNC path already names the distro and the Linux path unambiguously, so the information needed to take the shortcut is present at mount time. That would restore real POSIX permissions, ownership, and inotify for WSL-hosted workspaces — the dominant dev-container use case on this platform.

About

Run VS Code Dev Containers natively in WSL using WSL Containers (wslc.exe), without Docker Desktop

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages