System designHow-to

Camera USB passthrough in Docker: fix libusb errors

Fix libusb errors for USB camera passthrough in Docker: find the device node, pass it with --device, match the device group and verify with lsusb inside.

LumaSpec Vision10 min read Share on LinkedIn

On this page 10 sections
  1. 01Why does a camera that runs on the host fail with a libusb error in Docker?
  2. 02Which device node does my USB camera use?
  3. 03What is the shortest set of steps that makes it work?
  4. 04Which permissions does the container user need on the device node?
  5. 05When does a USB camera need a udev rule on the host?
  6. 06What changes on Docker Desktop for Windows or macOS?
  7. 07Does the same fix apply to an industrial USB3 Vision camera?
  8. 08What still breaks after the device is visible inside the container?
  9. 09When should the camera move from USB to Ethernet?
  10. 10Questions

A depth or color camera that streams cleanly on the host can fail within minutes of containerization: your capture stack raises a libusb error while the camera itself has not changed. The cause is almost never the code. A container sees only the device nodes you pass in, with the permissions you give it, plus the libusb runtime the image installs. This guide walks the fix for any USB camera on a native Linux host — depth, stereo and USB3 Vision area scan alike. You will find the node, pass it in with --device, match the group, verify with lsusb inside the container, and decide when a udev rule, a hot-plug rule or an Ethernet camera is the better answer.

Why does a camera that runs on the host fail with a libusb error in Docker?

The camera is fine — libusb inside the container cannot open the device node, because a container sees only the /dev/bus/usb nodes you explicitly pass in with --device, and a node that is passed but not readable returns the same access error.

libusb talks to character device nodes under /dev/bus/usb. USB devices carry major number 189 on Linux, which you can confirm with cat /proc/devices | grep usb. A container starts with its own /dev tree, so unless you pass the node or mount the directory, the file is not there and the open fails before your code runs a line. Even root inside the container is blocked without the pass-through, because Docker's device cgroup denies access to any node it was not handed. Host udev rules shape the host's /dev only; they do not travel with the image. That is why one Dockerfile works on one bench machine and fails on another — the node and its ownership differ.

Reproduce on the host first: if the SDK cannot open the camera outside Docker either, you have a driver problem, not a container problem — see USB camera in lsusb but not detected by the vision SDK on Linux.

Which device node does my USB camera use?

Run lsusb on the host and read the Bus and Device numbers: a camera listed as Bus 001 Device 007 lives at /dev/bus/usb/001/007, and that exact path is what you hand to the container.

$ lsusb
Bus 001 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 007: ID 1234:5678 USB depth camera

The two hex fields after ID are the vendor and product IDs; 1234:5678 is a placeholder — read your own pair from the line that names your camera. The node path follows directly: bus 001, device 007 becomes /dev/bus/usb/001/007.

$ ls -l /dev/bus/usb/001/007
crw-rw---- 1 root root 189, 7 Feb  3 14:05 /dev/bus/usb/001/007

That crw-rw---- opening is the mode: read and write for the owner and the group, nothing for everyone else. Write down the mode and group before touching Docker; libusb needs both read and write access to claim the camera's interfaces. If the camera also appears as /dev/video*, it is running the kernel's UVC class driver in parallel — Camera shows in lsusb but no video device: check the USB class explains which class yours uses.

What is the shortest set of steps that makes it work?

Pass the node with --device, install the libusb 1.0 runtime in the image, and run the container with the node's owning group ID: two flags and one package remove nearly every libusb access error a USB camera throws.

  1. Find the node. lsusb on the host gives Bus 001, Device 007 in our running example, so the path is /dev/bus/usb/001/007.

  2. Install the runtime in the image. usbutils provides lsusb inside the container, and the libusb 1.0 development package (Debian and Ubuntu call it libusb-1.0-0-dev) brings the runtime library your SDK bindings load:

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y --no-install-recommends \
      libusb-1.0-0-dev usbutils \
 && rm -rf /var/lib/apt/lists/*
  1. Run with the node and its group. stat -c %g prints the owning group's numeric ID on the host, and --group-add adds that ID to the container process, so the kernel's group check passes even though /etc/group inside the image knows nothing about it:
docker run --rm -it --name capture \
  --device=/dev/bus/usb/001/007 \
  --group-add $(stat -c %g /dev/bus/usb/001/007) \
  myimage:latest
  1. Verify inside the container before blaming your code. lsusb must print the same line the host printed, and a libusb open must succeed:
docker exec capture lsusb
docker exec capture python3 -c \
  "import usb.core; d = usb.core.find(idVendor=0x1234, idProduct=0x5678); print(d)"

Substitute your own ID pair for 0x1234 and 0x5678. find() prints a device object when the node is openable and None when it is not.

  1. If lsusb works but the SDK still raises an access error, prove it is permissions in one move. Run sudo chmod 666 /dev/bus/usb/001/007 on the host and rerun. A camera that now opens confirms the group-or-mode diagnosis; make it permanent with the udev rule below.

  2. Carry the same settings into compose so the fix survives CI and a teammate's checkout:

services:
  capture:
    image: myimage:latest
    devices:
      - "/dev/bus/usb/001/007:/dev/bus/usb/001/007"
    group_add:
      - "44"

Use the numeric GID printed by stat -c %g; docker run and compose pass it straight to the engine the same way.

Which permissions does the container user need on the device node?

The kernel checks the node's mode bits: a node readable only by root and its owning group rejects every other open, so you either add the owning group's GID with --group-add or loosen the mode to 0666 with a udev rule.

Three cases cover everything. Node not passed in: every open fails, root or not, at the cgroup. Node passed, container running as root: access works, because root bypasses mode bits. Node passed, image sets USER: the process needs the owning group's ID or a world-writable node. Match the case to your Dockerfile's USER line before changing anything.

The host-side equivalents for video and media nodes are covered in Linux camera device permissions: video and media node checks.

When does a USB camera need a udev rule on the host?

Add one when the fix must survive reboot, replug and a second engineer's session: a rule keyed to the camera's IDVendor and IDProduct sets the node's mode and group at every insertion, so the --device and --group-add values stop changing under you.

# /etc/udev/rules.d/99-usb-depth.rules
SUBSYSTEM=="usb", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", MODE="0660", GROUP="video", SYMLINK+="depthcam"

Apply it without a reboot:

sudo udevadm control --reload-rules
sudo udevadm trigger

Two details decide whether this works. ATTRS with the S walks up the device tree, and idVendor and idProduct live on the parent usb_device entry, so plain ATTR will not match. The 99- prefix runs the rule late, after the distribution's own rules. From then on pass the stable symlink — --device=/dev/depthcam survives a replug because udev recreates it at every insertion, though the container still needs a restart to pick up the fresh node.

What changes on Docker Desktop for Windows or macOS?

Docker Desktop runs every container inside a Linux virtual machine, so --device hands the container devices from that VM, and your physical USB port is not there: no flag fixes this from the desktop settings, and the practical answer is to run the capture container on a native Linux host.

A quick test tells you which world you are in: run lsusb inside the container. Only root hubs printed there, while the host prints your camera, means the device never reached the VM — nothing inside the image can change that. Under WSL2 the workable route is to attach the USB device into the VM with a USB-over-IP redirector first; the standard --device flow then works unchanged inside. On macOS there is no supported raw USB passthrough at all. For production, keep it plain: the capture container runs on the native Linux machine that sits next to the camera.

Does the same fix apply to an industrial USB3 Vision camera?

Yes — Linux has no kernel class driver for USB3 Vision, so the camera's GenTL producer opens the same /dev/bus/usb node through libusb, and the container needs the identical --device plus --group-add treatment as any depth camera.

UVC webcams are different: the kernel's uvcvideo driver claims them and exposes /dev/video* nodes, a separate passthrough covered in Camera in Docker: pass V4L2 media nodes, not just /dev/video0. Some depth cameras expose both paths; check ls -l /dev/video* next to your /dev/bus/usb node. A USB3 Vision producer must also be inside the image, with its CTI path set exactly as on the host — validate it before you enumerate, as in GenTL CTI file added but no error: validate before you enumerate.

Both catalog cameras below are C-mount USB3 area scan cameras that take exactly this route:

Area scan camerasHikrobot MV-CT0MU0060/R/NN/NN 6 MP Area Scan Camera, 3rd Gen., USB3.0, Mono, BASEMV-CT0MU0060/R/NN/NN
Sensor format
1/1.8"
Lens mount
C-mount
$969.00View part
Area scan camerasHikrobot MV-CS200-10UM 20 MP Area Scan Camera, USB3.0, IMX183, 2nd Gen., MonoMV-CS200-10UM
Sensor format
1"
Lens mount
C-mount
$759.00View part

Neither camera needs an extra kernel module in the image — only the producer, its CTI path and the libusb runtime you installed earlier.

What still breaks after the device is visible inside the container?

Bandwidth and contention move to the foreground: a USB 3.0 controller signals at 5 Gbit/s but delivers roughly 400 MB/s of usable payload, so two untriggered USB3 cameras on one controller can saturate the bus and drop frames.

usable payload ≈ signalling rate × 8/10 − protocol overhead USB 3.0: 5 Gbit/s × 0.8 = 4 Gbit/s raw ≈ 400 MB/s effective per controller

The arithmetic in practice: a 6 MP mono camera sends 6 MB per 8-bit frame, so 30 fps costs 180 MB/s. Two such cameras on one controller total 360 MB/s against about 400 MB/s available — 40 MB/s of headroom, which is why the second camera starts dropping frames when both run untriggered at full rate.

Stream timeouts that appear only under load are usually this ceiling, not the container; Diagnose USB camera color stream timeout across host ports covers the port-to-port diagnosis, and one controller per camera removes the ceiling entirely.

For hot-plug, whitelist the USB device class once and mount the directory instead of single nodes — every USB node shares major 189:

docker run --device-cgroup-rule='c 189:* rmw' \
  -v /dev/bus/usb:/dev/bus/usb \
  myimage:latest

A replug then needs no container restart; the kernel creates the new minor number and the rule already allows it. One more trap: a host-side test tool or a second container claiming the camera returns busy errors that look like libusb faults — one device, one opener, as Stale /dev/shm device lock when a second user opens the camera shows from the host side.

When should the camera move from USB to Ethernet?

Move the camera to GigE or 10GigE when the cable must exceed 3 m, the container host sits away from the camera, or more than two USB3 cameras share one controller: an Ethernet camera has no device node at all, only IP reachability from inside the container.

There is no libusb and no cgroup rule to manage; the container needs host networking or a macvlan address on the camera's subnet. Discovery broadcasts do not cross a default bridge network, which is the most common second failure — Ethernet 3D camera in a Docker container: fix discovery and streaming walks that end to end. The 3D sensors category and the camera finder filter by interface, so you can compare the GigE side against the USB3 side directly.

The CS200 line spans both transports: the MV-CS200-10GC is its 20 MP GigE, C-mount variant.

Area scan camerasHikrobot MV-CS200-10GC 20 MP Area Scan Camera, GigE, IMX183, Color, BasicMV-CS200-10GC
Sensor format
1"
Lens mount
C-mount
Interface
GigE
Resolution
16-25 MP
$739.00View partBuild a system with it

Next step: if lsusb now shows the camera inside the container and your application still fails, send us the SDK's exact error string through the contact page together with the node, its mode and its group — free expert support comes with every order, and more Linux-side fixes are collected on the systems topic page.

Questions

Which libusb error code means permission denied rather than a missing camera?

LIBUSB_ERROR_ACCESS, returned as -3, means the node exists but the open or claim was denied — fix permissions or groups. LIBUSB_ERROR_NO_DEVICE (-4) means the node vanished, usually a re-enumeration after replug. LIBUSB_ERROR_NOT_FOUND (-5) points at a wrong interface or descriptor request, and LIBUSB_ERROR_NOT_SUPPORTED (-12) at an operation the driver path does not implement. These codes come from the libusb 1.0 API.

Is --privileged an acceptable shortcut for USB cameras in Docker?

No for production. --privileged turns off Docker's device access control for everything, so any parsing bug in your capture stack becomes a host-level problem. Use --device for the one node you need and --group-add for its owning group; keep --privileged for throwaway diagnosis on a bench machine, never on a line-side controller.

Why does the --device path stop working after the camera is replugged?

The kernel assigns Bus and Device numbers in plug order, so /dev/bus/usb/001/007 can become /dev/bus/usb/001/009 after a replug while the container still points at the old path. Key a udev rule to the camera's idVendor and idProduct, pass a stable SYMLINK such as /dev/depthcam, or whitelist major 189 with --device-cgroup-rule and mount the whole /dev/bus/usb tree.

Can two containers share one USB camera?

Not at the same time. libusb claims the camera's interfaces exclusively, so the second opener gets LIBUSB_ERROR_BUSY (-6), whether it is another container or a host tool. Give each camera its own container, sequence the openers with a supervisor, or move to separate cameras — one device, one claim is the rule.

Does a GigE camera need the same setup in a container?

No node and no libusb: a GigE camera is an IP endpoint. Run the container with host networking or give it a macvlan address on the camera's subnet, set the camera to a static IP on that subnet, and raise the socket buffers for sustained streams — discovery broadcasts do not cross a default bridge network.