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.
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
--platformarguments 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
devcin any WSL project folder containing a.devcontainerto 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.
The wrapper script sits between VS Code and WSL Containers (wslc.exe), intelligently translating paths and mitigating interop crashes.
- Save
wslc_wrapper.shto a directory in your WSL$PATH(e.g.,~/.local/bin/wslc). - Make it executable:
chmod +x ~/.local/bin/wslc - Ensure that
~/.local/bincomes before/mnt/c/.../wslc.exein your$PATHso 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).
The devc bash function lets you bypass the VS Code UI and natively boot into a Dev Container context right from your WSL terminal.
- Open your
~/.bashrc(or~/.zshrc). - Paste the contents of
devc_launcher.shat the bottom of the file. - Reload your terminal:
source ~/.bashrc.
Navigate to any project with a .devcontainer configuration and simply type:
devcVS Code will immediately open, successfully build the container, inject the proper path formats, bypass the interop bugs, and connect you perfectly.
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.logEach 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.
Creating a seamless bridge between VS Code (Windows), WSL, and WSL Containers (wslc.exe) required untangling four overlapping technical bugs.
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.
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.
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.
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).
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.
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 byroot:root. The execute bit is meaningless — set on everything. chmod/chowninside 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.
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.shThe 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.