We're trying out using devcontainers in WSL to develop Zephyr based software for our products. For flashing products from within the devcontainer we're connecting the corresponding usb devices (dev kits or J-Links) to wsl with usbip, like described on microsofts website. We're then binding the /dev/bus/usb directory to the container and running it in privileged mode to be able to access usb devices from within the container. This works quite well, but we have one small issue with it:
When WSL first starts and there aren't any usb devices connected to it yet, the /dev/bus/usb directory isn't created yet. But we would like to start the devcontainer already and then connect a usb device at a later point. This is why we're using a volume mount, because then docker will just create the directory. When we then connect a usb device to wsl it also shows up in the devcontainer and we can see it with "nrfutil device list" and flash it with "west flash".
However, the nrf connect extension doesn't recognize it. When clicking the refresh button in the device list, the following error in the extension host output gets printed:
2025-09-30 09:42:29.373 [error] Error: kill ESRCH at process.kill (node:internal/process/per_thread:235:13) at ol (/home/user/.vscode-server/extensions/nordic-semiconductor.nrf-connect-2025.9.798-linux-x64/dist/extension.js:298:4217) at kue.restartProcess (/home/user/.vscode-server/extensions/nordic-semiconductor.nrf-connect-2025.9.798-linux-x64/dist/extension.js:402:7782) at pce.refreshDevicesCommand (/home/user/.vscode-server/extensions/nordic-semiconductor.nrf-connect-2025.9.798-linux-x64/dist/extension.js:316:4845) at /home/user/.vscode-server/extensions/nordic-semiconductor.nrf-connect-2025.9.798-linux-x64/dist/extension.js:397:3433 at n (/home/user/.vscode-server/extensions/nordic-semiconductor.nrf-connect-2025.9.798-linux-x64/dist/extension.js:290:10957) at Kb.h (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:112:41565) at Kb.$executeContributedCommand (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:112:42456) at j4.S (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:168242) at j4.Q (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:168022) at j4.M (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:167111) at j4.L (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:166349) at od.value (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:165013) at $.C (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:27:2373) at $.fire (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:27:2591) at vo.fire (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:9458) at od.value (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:391:8617) at $.C (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:27:2373) at $.fire (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:27:2591) at vo.fire (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:9458) at iv.A (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:12574) at od.value (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:10994) at $.C (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:27:2373) at $.fire (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:27:2591) at y5.acceptChunk (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:7941) at od.value (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:7227) at $.C (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:27:2373) at $.fire (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:27:2591) at pL.z (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:21744) at pL.acceptFrame (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:21550) at fL.n (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:20078) at file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:17310 at Socket.t (file:///vscode/vscode-server/bin/linux-x64/e3a5acfb517a443235981655413d566533107e92/out/vs/workbench/api/node/extensionHostProcess.js:29:15251) at Socket.emit (node:events:518:28) at addChunk (node:internal/streams/readable:561:12) at readableAddChunkPushByteMode (node:internal/streams/readable:512:3) at Readable.push (node:internal/streams/readable:392:5) at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
Steps to reproduce:
- Start wsl and make sure no usb devices are attached to wsl
- Create a new directory and copy the attached Dockerfile and .devcontainer.json into it
- Open the directory with vscode and reopen it in the devcontainer when prompted
- Once vscode finished initializing, attach a usb device for nrf connect to wsl (an nrf52840 dev kit in our case)
- Observe that "nrfutil device list" finds the device:
1050241217 Product J-Link Board version PCA10056 Traits seggerUsb, usb, devkit, jlink Supported devices found: 1
- Try to refresh the "Conntected Devices" in the nrf connect extension and observe the error message in the Output for the "Extension Host (Remote)"
Before attaching the usb device, "nrfutil device list" prints:
Error: Failed to enumerate devices
Caused by:
/sys/bus/usb/devices/ not found (errno 2) (UsbLister)
It looks to me like refreshing the connected devices tries to kill the previous "nrfutil device list" process to restart it, which doesn't work because it doesn't exist anymore because it crashed the first time because /sys/bus/usb/devices/ didn't exist yet. So my naive solution would be to simply ignore any errors when trying to kill the old "nrfutil device list" process.
Since it doesn't let me attach the Dockerfile, here the contents of the Dockerfile:
# 0.27.5 is the last version that includes the Zephyr SDK 0.17.0, Zephyr SDK versions above that don't work with the nrf SDK 3.0.2
FROM zephyrprojectrtos/ci:v0.27.5
# nrfutil wants version 8.42
ARG JLINK_VERSION=8.42
# install packages
RUN <<EOT
set -e
apt-get update -y
apt-get install -y --no-install-recommends \
cppcheck \
fish \
less \
udev \
usbutils \
stow
apt-get autoremove -y
apt-get clean -y
rm -rf /var/lib/apt/lists/*
EOT
# install jlink
RUN <<EOT
set -e
apt-get update -y
JLINK_DEB_FILE=JLink_Linux_V${JLINK_VERSION/./}_x86_64.deb
wget --post-data="accept_license_agreement=accepted&non_emb_ctr=confirmed" https://www.segger.com/downloads/jlink/$JLINK_DEB_FILE
# we can't install jlink directly, as it expects udevadm to run to reload udev rules after installing
# so we install manually and remove the post-install script
dpkg --unpack ./$JLINK_DEB_FILE
rm -f /var/lib/dpkg/info/jlink.postinst
apt-get install -f -y
rm $JLINK_DEB_FILE
apt-get clean -y
rm -rf /var/lib/apt/lists/*
EOT
RUN pip3 install codechecker --no-cache-dir
# install nrf udev rules
RUN <<EOT
set -e
wget https://github.com/NordicSemiconductor/nrf-udev/releases/download/v1.0.1/nrf-udev_1.0.1-all.deb
dpkg -i nrf-udev_1.0.1-all.deb
rm nrf-udev_1.0.1-all.deb
EOT
# install nrfutil
RUN <<EOT
set -e
wget https://files.nordicsemi.com/artifactory/swtools/external/nrfutil/executables/x86_64-unknown-linux-gnu/nrfutil
mv nrfutil /usr/bin/
chmod +x /usr/bin/nrfutil
EOT
# install nrfutil device command as user
USER user
RUN <<EOT
set -e
nrfutil self-upgrade
nrfutil install device
EOT
USER root
# persist command history across sessions
RUN mkdir /commandhistory && chown -R user:user /commandhistory
USER user
RUN <<EOT
set -e
# bash history
touch /commandhistory/.bash_history
ln -s /commandhistory/.bash_history /home/user/.bash_history
# fish history
mkdir -p /home/user/.local/share/fish
touch /commandhistory/fish_history
ln -s /commandhistory/fish_history /home/user/.local/share/fish/fish_history
EOT
USER root
And the .devcontainer.json file:
{
"build": {
"dockerfile": "Dockerfile"
},
"remoteUser": "user",
"mounts": [
// persist command history across sessions
"source=commandhistory,target=/commandhistory,type=volume"
],
"customizations": {
"vscode": {
"extensions": [
"nordic-semiconductor.nrf-connect-extension-pack"
]
}
},
"runArgs": [
// Allow the container to use usb devices
"--privileged",
// This is a volume instead of a bind mount because the /dev/bus/usb directory doesn't
// exist on the host if there's no usb device connected to the host and the bind mount
// expects the directory to exist on the host, causing an error when trying to use the dev
// container without any connected usb devices.
// Binding the path as a volume fixes this, because the directory on the host will simply
// be created if it doesn't exist.
"--volume=/dev/bus/usb:/dev/bus/usb"
]
}