System designHow-to

Pass a USB camera into containers with a CDI spec

Map every USB node a multi-interface camera enumerates, write a CDI spec for it and pass it into Docker or Podman without sharing all of /dev.

LumaSpec Vision8 min read Share on LinkedIn

On this page 10 sections
  1. 01What a CDI spec is
  2. 02What you need
  3. 03Step 1: inventory every node the camera enumerates
  4. 04Step 2: check the udev rules that give you access
  5. 05Step 3: write the CDI spec
  6. 06Step 4: run the container and verify isolation
  7. 07Step 5: keep it working under change
  8. 08When CDI is the wrong tool
  9. 09Quick checklist
  10. 10Questions

USB3 cameras that expose several interfaces at once are awkward guests in containers. Pass the obvious node with --device and the acquisition library still fails, because the camera enumerated three or four nodes and you shared one. Pass all of /dev and everything runs, at the cost of giving the container the host's disks, serial ports and USB controllers. This guide shows the middle path: identify every node a camera enumerates, write a Container Device Interface (CDI) spec for it, and hand the container exactly that device and nothing more. If your frames never arrive at all inside the container, start with GenTL camera discovery in containers: diagnose missing devices before writing a spec.

What a CDI spec is

CDI is a vendor-neutral specification that describes, in one JSON file, every resource a container needs to use a device: device nodes, mounts and environment entries. The runtime reads the file, creates the nodes inside the container and applies the permissions. GPU vendors use it to expose complex hardware cleanly, and the same mechanism suits a multi-interface USB camera well: one spec, one command line flag, no --privileged and no shared /dev.

Runtime support is now common: recent Docker and Podman releases both accept --cdi. Specs live in /etc/cdi/ or /var/run/cdi/ with names like camera.example.org.json, and the qualifier before .json becomes the device name you pass at run time.

What you need

  1. A Linux host with the camera's vendor runtime and udev rules installed, and the camera recognized by a native application outside any container. Fix that first if not; a spec cannot rescue a camera the host cannot open.
  2. A container runtime with CDI support, and jq plus lsusb for the inventory steps.
  3. An image that contains the camera's acquisition library or GenTL producer.
  4. A USB3 camera of the sort you would actually deploy, for example the 12 MP USB3 area scan camera or the 6 MP USB3 board-replacement-class camera. Both enumerate as standard USB3 Vision devices, so the procedure below applies unchanged.
Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
$560.00View part
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"
Interface
USB3
$969.00View part

Step 1: inventory every node the camera enumerates

  1. Run lsusb twice: once with the camera unplugged, once plugged in.
  2. The lines that appear only in the second run belong to the camera. A multi-interface camera typically shows 2 to 5 entries, often split across a hub chip and the sensor interface itself.
  3. Record the vendor:product ID of each, for example 0e70:001f.
  4. List the nodes each entry created:
ls -l /dev/bus/usb/*/* | grep -i <idVendor-or-description>
ls /dev/video* /dev/media* 2>/dev/null

Many machine-vision cameras create no video4linux nodes at all: a USB3 Vision camera talks to its SDK over libusb directly, so the only nodes are the raw /dev/bus/usb/<bus>/<dev> entries. Others, especially depth and RGB cameras, expose video* and media* nodes too. Write down every path. Missing one is the usual reason a half-shared container half-works.

Step 2: check the udev rules that give you access

  1. Confirm which rule file matches your camera:
grep -r <idVendor> /usr/lib/udev/rules.d/ /etc/udev/rules.d/
  1. Read the rule: it decides who may open the device inside the container. If it grants access to a group such as plugdev or a vendor group, run the container with --group-add <gid> for that group, or mount the rule and reload it.
  2. Test by opening the device as the user the container will run as, before you touch the container at all.

Forgetting the udev side is the second most common failure: the node appears inside the container but opening it returns "access denied".

Step 3: write the CDI spec

  1. Create /etc/cdi/vision.example.com.json. The structure is one JSON object with a device list; each device names nodes, mounts and environment entries. A minimal working shape:
{
  "cdiVersion": "0.6.0",
  "kind": "vision.example.com/cam",
  "devices": [{
    "name": "usb3cam",
    "containerEdits": {
      "deviceNodes": [
        {"path": "/dev/bus/usb/001/004"},
        {"path": "/dev/video0"},
        {"path": "/dev/media0"}
      ],
      "env": ["GENICAM_GENTL64_PATH=/opt/vision/gentl"]
    }
  }]
}
  1. Note what each part does: kind plus device name form the selector you pass as --cdi vision.example.com/cam=usb3cam. List every node from step 1 in deviceNodes. For a plain USB3 Vision camera that is usually just the /dev/bus/usb node; depth cameras add the video* and media* entries.
  2. Put the paths your SDK needs at run time in env, such as the GenTL producer directory. The same variables you would set on the host belong here.
  3. If your camera needs a vendor daemon or shared library mounted from the host, add a mounts entry; keep it to the vendor's own install path only.
  4. Validate that the runtime sees the spec:
cdi validate /etc/cdi/vision.example.com.json

Step 4: run the container and verify isolation

  1. Start the container with the spec instead of --device:
docker run --rm --cdi vision.example.com/cam=usb3cam my-acq-image \
  ls /dev/bus/usb/*/*
  1. Verify the camera is usable: run your acquisition tool inside the container and confirm it enumerates and grabs frames at the expected resolution and frame rate. A 4000 × 3000 sensor at 30 fps should deliver the same 30 fps inside the container as outside; if it does not, suspect buffering, not the spec.
  2. Verify the isolation holds: ls /dev inside the container should show only your camera's nodes, not the host's disks or other USB devices.

A quick bandwidth sanity check belongs here too. USB3 offers about 5 Gbps at the wire, and a 12 MP, 30 fps stream pushes roughly 700 MB/s before protocol overhead, which is why these cameras are USB3 rather than USB2 and why the container must not add copies of the frame buffer on the way to your code. Grab, process, release.

Step 5: keep it working under change

  1. Re-run the lsusb diff after any camera firmware or SDK update: vendors occasionally add an interface, which adds an ID and a node.
  2. Keep one spec file per camera model in version control next to your deployment scripts, and deploy to /etc/cdi/ with the rest of the host configuration.
  3. Test the spec after every runtime upgrade; CDI versions evolve and the container engine's support can lag the spec file's cdiVersion.

When CDI is the wrong tool

Not every camera needs this. A GigE area scan camera needs no device nodes at all: it is a network device, so the container wants --network host (or a macvlan interface) plus the GenTL producer mounted inside. The isolation conversation then moves to network policy, not /dev. Depth and stereo products in the 3D sensors category split both ways: structured-light cameras over USB benefit directly from a spec, while Ethernet-attached profilers do not. If your camera talks Ethernet, spend the effort on the network namespace instead.

There is also a middle case: if you control the whole host and only run one container, sharing a single --device per node may be enough. CDI earns its keep when several containers share a host, when a security policy demands least-privilege device access, or when you want a repeatable, reviewable description of what a camera needs rather than a long docker run incantation. For the host that runs it all, a vision controller with camera ports keeps acquisition hardware out of the container question entirely; see the vision controllers category.

Quick checklist

  • [ ] lsusb diff with and without the camera: record every vendor and product ID.
  • [ ] List all /dev/bus/usb, video* and media* nodes the camera creates.
  • [ ] Locate the vendor udev rule and note the group it grants.
  • [ ] Write the CDI spec with every node plus the SDK environment variables.
  • [ ] Validate the spec, run with --cdi, and confirm frames arrive.
  • [ ] Confirm ls /dev inside the container shows the camera and nothing else.
  • [ ] Re-test after firmware, SDK or runtime updates.

Choosing the camera itself is a separate exercise: resolution and sensor format drive the lens, interface and bandwidth decisions, and the camera configurator narrows the catalog by those filters. If your containers run on an ARM board or industrial mini PC, plan the SDK binaries for that architecture before you containerize. For long runs from the camera to the host, check the USB cables you specify: an under-specified cable drops frames long before the container does. For the host side of a multi-camera cell, the same CDI pattern extends one spec per device.

Questions

Why not just pass --device=/dev to the container?

Sharing /dev gives the container access to every device on the host: disks, USB controllers, GPUs and serial ports. That defeats most of the isolation a container provides, and many security policies forbid it. A CDI spec exposes only the nodes, device permissions and environment entries one camera actually needs, so the container can use the camera and nothing else.

Do I need one CDI spec per camera model?

Yes, in practice. Each camera model enumerates its own set of vendor and product IDs and its own node names. Copy a working spec and change the IDs when you introduce a different camera. If your cell mixes models, ship one spec file per model and select with --cdi at run time.

Does CDI work with Podman as well as Docker?

Yes. CDI is a container-runtime-level specification, so any runtime that implements it can consume the same spec file. Podman, Docker and several Kubernetes device plugins read specs from the standard /etc/cdi or /var/run/cdi directories. You write the spec once and use the same file in development and on the line.

What if my camera uses GigE instead of USB?

GigE Vision cameras need no device passthrough at all: the container only needs a host network interface, so the usual approach is --network host plus a mounted GenTL producer and environment variables. Device isolation is then a network question, not a /dev question, and a CDI spec is normally unnecessary.

Where do the udev rules come from?

The camera manufacturer documents and ships them; install the vendor runtime package on the host and the rules land in /etc/udev/rules.d or /usr/lib/udev/rules.d. Never invent your own permissions for a device the vendor's driver manages. Copy the matching rule paths into your CDI spec so the container-side mount points at the same files the host uses.