Adding USB devices to your containers

While this may seem like an uncommon scenario, it’s very useful to add USB devices to your containers. Imagine running Home Assistant in your container and having a Z-wave or Zigbee USB stick in the container host. There are multiple ways to add this, and it can be a bit confusing if you don’t know the difference between the various methods. In this blog I’ll descibe two ways of setting up USB devices to your container.

The first method is by far the easiest: simply add ‘privileged: true’ to your docker-compose file or add –privileged as a flag to the docker run command. This allows the container to access all components of the host which runs your docker runtime engine. Obviously this is not preferred, as your container can reach every block device on your system and could even reach passwords kept in memory. If you don’t care about this level of security, this method is by far the easiest solution.

The second method is much more secure but can be a bit confusing. Simply add the device you need (e.g. /dev/ttyUSB1) as a virtual device inside your container. In docker compose this looks like this:

devices:
  - /dev/ttyUSB1:/dev/ttyUSB1

Note that this is a simple mapping, using <actual_path_on_host>:<virtual_path_in_container>. To keep things this simple, we used the same virtual path as the actual path. Easy right? There is a drawback though: Linux will randomly generate the devicename of your USB stick on reboot or after adding a USB stick. This is why you should never add a /dev/<device> directly, but rather use the symlinks which contain a unique identifier for your USB device.

Let’s take a look at my ZZH! stick on the docker host. Using ‘dmesg’ you can see it’s mapped to ‘ttyUSB1’

[1199309.256461] usb 2-1.2: New USB device strings: Mfr=0, Product=2, SerialNumber=0
 [1199309.256463] usb 2-1.2: Product: USB Serial
 [1199309.256893] ch341 2-1.2:1.0: ch341-uart converter detected
 [1199309.257777] usb 2-1.2: ch341-uart converter now attached to ttyUSB1

so you can find it at /dev/ttyUSB1. However this can change randomly as stated above, so rather let’s look at /dev/serial/by-id

ls -lthra /dev/serial/by-id
total 0
drwxr-xr-x 4 root root 80 Dec 20 16:46 ..
lrwxrwxrwx 1 root root 13 Dec 20 16:46 usb-0658_0200-if00 -> ../../ttyACM1
lrwxrwxrwx 1 root root 13 Dec 20 16:46 usb-RFXCOM_RFXtrx433_A1YUV98W-if00-port0 -> ../../ttyUSB0
lrwxrwxrwx 1 root root 13 Jan 3 13:54 usb-1a86_USB_Serial-if00-port0 -> ../../ttyUSB1
drwxr-xr-x 2 root root 100 Jan 3 13:54 .

here you can see I have multiple USB adapters connected, and

/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0

is the device which always points to my ZZH! stick.
So let’s add this symlink to the docker compose file, and to prevent confusion, let’s rename the target virtual mapping inside the container:

devices:
  - /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0:/dev/Virtual_ZZH_stick

Now the container will have a /dev/Virtual_ZZH_stick device which automatically maps to the actual USB stick, even after reboot or swapping USB ports. Note that any configuration you might have in the container, must now point to /dev/Virtual_ZZH_stick. For example, my zigbee2mqtt container will now have:

serial:
  port: /dev/Virtual_ZZH_stick

Note that you could add both methods 1 and 2 above, but it would make little sense: using privileged: true you wouldn’t need any device mappings and such it seems to be ignored during my many debugging hours. As such, I’d recommend the following:

  1. After plugging in your usb stick, run ‘dmesg’ to see which mapping it contains.
  2. Add privileged: true to see if the container can reach the device. If this is not the case, check if the privileges are set properly on the /dev/<mapping> location. You could temporarily set ‘chmod 777 /dev/<mapping>’ to see if this fixes your issue.
  3. If the container can reach the device, remove privileged: true from your container configuration and use the device mapping as suggested by device 2.