A step-by-step guide to connect any USB device (cameras, serial adapters, robots) from Windows through WSL to Docker containers.
The flow is always the same:
- Windows → Find and share the USB device with
usbipd - WSL → Attach the device so Linux can see it
- Docker → Map the Linux device into your container
- Restart → Reattach after reboot (binding usually persists, but attachment doesn't)
- Windows 11 with WSL 2 installed
usbipd-wininstalled on Windows (install guide)- Docker Desktop running on WSL 2
- USB device connected to Windows
Open PowerShell (not necessarily as Admin yet):
usbipd listExample output:
Connected:
BUSID VID:PID DEVICE STATE
2-10 5986:211b HD Webcam Not shared
2-14 8087:0026 Intel(R) Wireless Bluetooth(R) Not shared
3-1 0bda:8153 Realtek USB GbE Family Controller Not shared
What it means:
BUSID= identifier you use inbindandattachcommands (e.g.,2-10)VID:PID= vendor and product IDsSTATE=Not sharedmeans not yet available to WSLSTATE=Sharedmeans already bound but may not be attached yet
Note your device's BUSID — you will need it in the next steps.
Open PowerShell as Administrator and run:
usbipd bind --busid <BUSID>Example:
usbipd bind --busid 2-10What it does:
- Shares the USB device so WSL can access it
- Changes the Windows driver mode to allow USB/IP passthrough
- Only needs to be done once per device (persists across reboots usually)
If it fails with "Device busy":
- Close any Windows app using the device (Camera app, Teams, browser tabs, OBS, vendor software, DISCORD !!)
- Retry the bind command
If you need to force it:
usbipd bind --busid <BUSID> --forceNote: --force may require a reboot afterward.
Run:
usbipd attach --wsl --busid <BUSID>Example:
usbipd attach --wsl --busid 2-10What it does:
- Makes the device available inside your WSL 2 distribution
- Creates a virtual USB connection through USB/IP protocol
- Not persistent — you usually need to reattach after reboot or device reconnection
Success output:
usbipd: info: Using WSL distribution 'Ubuntu' to attach; the device will be available in all WSL 2 distributions.
usbipd: info: Loading vhci_hcd module.
usbipd: info: Detected networking mode 'nat'.
usbipd: info: Using IP address 172.17.96.1 to reach the host.
If it fails with "Device busy":
- Close Windows apps using the device
- Try again
If it still fails after closing apps:
- The device may still be claimed by Windows drivers
- Try
usbipd bind --busid <BUSID> --force, then reboot, then attach again
Open WSL terminal and check what Linux sees:
lsusbExample output for a camera:
Bus 001 Device 002: ID 5986:211b Acer, Inc HD Webcam
For cameras specifically:
ls /dev/video*For serial/USB adapters:
ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/nullFor generic device info:
dmesg | tail -n 50If device doesn't appear:
- Check that
usbipd attachcompleted successfully in PowerShell - Verify with
usbipd liston Windows that state shows attachment - In WSL, try:
sudo modprobe usbip_host - Rerun attach in PowerShell
v4l2-ctl --device=/dev/video0 --all
v4l2-ctl --device=/dev/video0 --list-formats-ext
v4l2-ctl --device=/dev/video0 --list-ctrlsudevadm info -a -n /dev/ttyUSB0lsusb -v -d <VID>:<PID>Example for the Acer camera (VID=5986, PID=211b):
lsusb -v -d 5986:211bAdd the devices: section to your service. The device path is the Linux path from WSL (e.g., /dev/video0).
Example for a camera:
version: '3.9'
services:
graspgen:
image: your-image:latest
devices:
- "/dev/video0:/dev/video0"
# ... rest of your service configExample for a serial device:
services:
robot-controller:
image: your-image:latest
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0"
# ... rest of your service configExample for multiple devices:
services:
my-app:
image: your-image:latest
devices:
- "/dev/video0:/dev/video0"
- "/dev/video1:/dev/video1"
- "/dev/ttyUSB0:/dev/ttyUSB0"
# ... rest of your service configdocker compose down
docker compose up -ddocker compose exec <service_name> bashInside the container:
ls /dev/video0
lsusbffmpeg -f v4l2 -input_format mjpeg -video_size 640x480 -framerate 30 -i /dev/video0 -frames:v 1 -y test.jpgOr with OpenCV:
import cv2
import time
cap = cv2.VideoCapture("/dev/video0", cv2.CAP_V4L2)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG'))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)
time.sleep(1.0)
ret, frame = cap.read()
print("opened =", cap.isOpened(), "ret =", ret, "shape =", None if frame is None else frame.shape)
if ret:
cv2.imwrite("test.jpg", frame)
cap.release()stty -F /dev/ttyUSB0 115200
cat /dev/ttyUSB0After you restart your computer, USB devices detach automatically. Use this quick checklist to reconnect:
usbipd listCheck if your device shows Shared or Not shared.
If Not shared:
usbipd bind --busid <BUSID>Always run (regardless of state):
usbipd attach --wsl --busid <BUSID>lsusb
ls /dev/video* 2>/dev/null # for cameras
ls /dev/ttyUSB* 2>/dev/null # for serialdocker compose down
docker compose up -dCause: Windows app or service still using the device.
Solution:
- Close Camera app, Teams, Zoom, browser tabs, OBS, vendor software
- Check Task Manager for any process using the camera
- Retry:
usbipd attach --wsl --busid <BUSID>
Cause: Device recognized at USB level but kernel driver not loaded or negotiated.
Solution:
sudo apt update
sudo apt install -y v4l-utils usbutils
sudo modprobe usbip_hostThen detach and reattach in PowerShell.
Cause: Device path in docker-compose.yml doesn't exist in WSL.
Solution:
- Inside container:
ls /dev/video*to confirm what device is actually there - Update
devices:indocker-compose.ymlto match - Run
docker compose up -dagain
Cause: OpenCV V4L2 backend needs explicit format settings.
Solution: Use this pattern:
import cv2
import time
cap = cv2.VideoCapture("/dev/video0", cv2.CAP_V4L2)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG'))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)
time.sleep(1.0) # warmup time
ret, frame = cap.read()
cap.release()Cause: Passthrough path is unstable at higher bandwidth.
Solution:
- Use
640x480as your stable mode - Test if the camera works at 720p on native Windows or Linux (without WSL passthrough)
- If it works elsewhere, the limitation is the USB/IP passthrough layer, not the camera
- Consider FFmpeg or ROS camera driver as alternative capture methods
Windows:
usbipd list # Find: 2-10 5986:211b HD Webcam
usbipd bind --busid 2-10
usbipd attach --wsl --busid 2-10WSL:
lsusb # Should show: ID 5986:211b Acer, Inc HD Webcam
ls /dev/video* # Should show: /dev/video0 /dev/video1 /dev/media0
v4l2-ctl --device=/dev/video0 --list-formats-extdocker-compose.yml:
services:
graspgen:
devices:
- "/dev/video0:/dev/video0"Test in container:
ffmpeg -f v4l2 -input_format mjpeg -video_size 640x480 -framerate 30 -i /dev/video0 -frames:v 1 -y test.jpgWindows:
usbipd list # Find: 3-1 0403:6001 USB Serial Device
usbipd bind --busid 3-1
usbipd attach --wsl --busid 3-1WSL:
lsusb # Should show the serial device
ls /dev/ttyUSB* # Should show: /dev/ttyUSB0 (or higher number)docker-compose.yml:
services:
robot_controller:
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0"Test in container:
stty -F /dev/ttyUSB0 115200 # Set baud rate
cat /dev/ttyUSB0 # Read dataWindows:
usbipd list # Find Orbbec devices
usbipd bind --busid <BUSID>
usbipd attach --wsl --busid <BUSID>WSL:
lsusb # Should show Orbbec device
ls /dev/video* # Multiple video nodes expecteddocker-compose.yml:
services:
vision_app:
devices:
- "/dev/video0:/dev/video0"
- "/dev/video1:/dev/video1"
- "/dev/media0:/dev/media0"# List all devices
usbipd list
# Share a device (one-time, persists usually)
usbipd bind --busid 2-10
# Attach to WSL (repeat after reboot)
usbipd attach --wsl --busid 2-10
# Force bind if stuck
usbipd bind --busid 2-10 --force
# After --force, reboot Windows, then:
# usbipd list
# usbipd attach --wsl --busid 2-10# List USB devices
lsusb
# List video devices (cameras)
ls /dev/video*
# List serial devices
ls /dev/ttyUSB*
# Camera capabilities
v4l2-ctl --device=/dev/video0 --all
v4l2-ctl --device=/dev/video0 --list-formats-ext
# Kernel messages
dmesg | tail -n 50
# Device attributes
udevadm info -a -n /dev/video0# Down and up with new device mappings
docker compose down
docker compose up -d
# Access container
docker compose exec <service> bash
# Inside container, verify device
ls /dev/video0
lsusb- Always use explicit device paths in Docker, not indices (e.g.,
/dev/video0instead of0) - Set camera formats explicitly in code (FOURCC, width, height, fps) to avoid timeouts
- Use FFmpeg for initial testing if OpenCV times out
- Document your working modes (e.g., "640x480 MJPEG @ 30 fps is stable, 1280x720 times out")
- Create a shell script to automate bind + attach after reboots
- Avoid unplugging devices mid-session; detach in Windows first
Create a file attach_usb.ps1:
# attach_usb.ps1
# Usage: .\attach_usb.ps1 2-10
param(
[string]$BusId = "2-10"
)
Write-Host "Binding USB device $BusId..."
usbipd bind --busid $BusId
Write-Host "Attaching to WSL..."
usbipd attach --wsl --busid $BusId
Write-Host "Verifying in WSL..."
wsl lsusb
Write-Host "Done!"Run after reboot:
.\attach_usb.ps1 2-10Last updated: March 17, 2026
For your robotics thesis: Use this guide as a template. Update BUSID and device paths for your specific hardware, and save this alongside your project documentation.