[docker] Docker - a way to give access to a host USB or serial device?

Last time I checked, Docker didn't have any means to give container access to host serial or USB port. Is there a trick which allows doing that?

This question is related to docker

The answer is


It's hard for us to bind a specific USB device to a docker container which is also specific. As you can see, the recommended way to achieve is:

docker run -t -i --privileged -v /dev/bus/usb:/dev/bus/usb ubuntu bash

It will bind all the devices to this container. It's unsafe. Every containers were granted to operate all of them.

Another way is binding devices by devpath. It may looks like:

docker run -t -i --privileged -v /dev/bus/usb/001/002:/dev/bus/usb/001/002 ubuntu bash

or --device (better, no privileged):

docker run -t -i --device /dev/bus/usb/001/002 ubuntu bash

Much safer. But actually it is hard to know what the devpath of a specific device is.

I have wrote this repo to solve this problem.

https://github.com/williamfzc/usb2container

After deploying this server, you can easily get all the connected devices' information via HTTP request:

curl 127.0.0.1:9410/api/device

and get:

{
    "/devices/pci0000:00/0000:00:14.0/usb1/1-13": {
        "ACTION": "add",
        "DEVPATH": "/devices/pci0000:00/0000:00:14.0/usb1/1-13",
        "DEVTYPE": "usb_device",
        "DRIVER": "usb",
        "ID_BUS": "usb",
        "ID_FOR_SEAT": "xxxxx",
        "ID_MODEL": "xxxxx",
        "ID_MODEL_ID": "xxxxx",
        "ID_PATH": "xxxxx",
        "ID_PATH_TAG": "xxxxx",
        "ID_REVISION": "xxxxx",
        "ID_SERIAL": "xxxxx",
        "ID_SERIAL_SHORT": "xxxxx",
        "ID_USB_INTERFACES": "xxxxx",
        "ID_VENDOR": "xxxxx",
        "ID_VENDOR_ENC": "xxxxx",
        "ID_VENDOR_FROM_DATABASE": "",
        "ID_VENDOR_ID": "xxxxx",
        "INTERFACE": "",
        "MAJOR": "189",
        "MINOR": "119",
        "MODALIAS": "",
        "PRODUCT": "xxxxx",
        "SEQNUM": "xxxxx",
        "SUBSYSTEM": "usb",
        "TAGS": "",
        "TYPE": "0/0/0",
        "USEC_INITIALIZED": "xxxxx",
        "adb_user": "",
        "_empty": false,
        "DEVNAME": "/dev/bus/usb/001/120",
        "BUSNUM": "001",
        "DEVNUM": "120",
        "ID_MODEL_ENC": "xxxxx"
    },
    ...
}

and bind them to your containers. For example, you can see the DEVNAME of this device is /dev/bus/usb/001/120:

docker run -t -i --device /dev/bus/usb/001/120 ubuntu bash

Maybe it will help.


I wanted to extend the answers already given to include support for dynamically connected devices that aren't captured with /dev/bus/usb and how to get this working when using a Windows host along with the boot2docker VM.

If you are working with Windows, you'll need to add any USB rules for devices that you want Docker to access within the VirtualBox manager. To do this you can stop the VM by running:

host:~$ docker-machine stop default

Open the VirtualBox Manager and add USB support with filters as required.

Start the boot2docker VM:

host:~$ docker-machine start default

Since the USB devices are connected to the boot2docker VM, the commands need to be run from that machine. Open up a terminal with the VM and run the docker run command:

host:~$ docker-machine ssh
docker@default:~$ docker run -it --privileged ubuntu bash

Note, when the command is run like this, then only previously connected USB devices will be captures. The volumes flag is only required if you want this to work with devices connected after the container is started. In that case, you can use:

docker@default:~$ docker run -it --privileged -v /dev:/dev ubuntu bash

Note, I had to use /dev instead of /dev/bus/usb in some cases to capture a device like /dev/sg2. I can only assume the same would be true for devices like /dev/ttyACM0 or /dev/ttyUSB0.

The docker run commands will work with a Linux host as well.


Another option is to adjust udev, which controls how devices are mounted and with what privileges. Useful to allow non-root access to serial devices. If you have permanently attached devices, the --device option is the best way to go. If you have ephemeral devices, here's what I've been using:

1. Set udev rule

By default, serial devices are mounted so that only root users can access the device. We need to add a udev rule to make them readable by non-root users.

Create a file named /etc/udev/rules.d/99-serial.rules. Add the following line to that file:

KERNEL=="ttyUSB[0-9]*",MODE="0666"

MODE="0666" will give all users read/write (but not execute) permissions to your ttyUSB devices. This is the most permissive option, and you may want to restrict this further depending on your security requirements. You can read up on udev to learn more about controlling what happens when a device is plugged into a Linux gateway.

2. Mount in /dev folder from host to container

Serial devices are often ephemeral (can be plugged and unplugged at any time). Because of this, we can’t mount in the direct device or even the /dev/serial folder, because those can disappear when things are unplugged. Even if you plug them back in and the device shows up again, it’s technically a different file than what was mounted in, so Docker won’t see it. For this reason, we mount the entire /dev folder from the host to the container. You can do this by adding the following volume command to your Docker run command:

-v /dev:/dev

If your device is permanently attached, then using the --device option or a more specific volume mount is likely a better option from a security perspective.

3. Run container in privileged mode

If you did not use the --device option and mounted in the entire /dev folder, you will be required to run the container is privileged mode (I'm going to check out the cgroup stuff mentioned above to see if this can be removed). You can do this by adding the following to your Docker run command:

--privileged

4. Access device from the /dev/serial/by-id folder

If your device can be plugged and unplugged, Linux does not guarantee it will always be mounted at the same ttyUSBxxx location (especially if you have multiple devices). Fortunately, Linux will make a symlink automatically to the device in the /dev/serial/by-id folder. The file in this folder will always be named the same.

This is the quick rundown, I have a blog article that goes into more details.


With latest versions of docker, this is enough:

docker run -ti --privileged ubuntu bash

It will give access to all system resources (in /dev for instance)


If you would like to dynamically access USB devices which can be plugged in while the docker container is already running, for example access a just attached usb webcam at /dev/video0, you can add a cgroup rule when starting the container. This option does not need a --privileged container and only allows access to specific types of hardware.

Step 1

Check the device major number of the type of device you would like to add. You can look it up in the linux kernel documentation. Or you can check it for your device. For example to check the device major number for a webcam connected to /dev/video0, you can do a ls -la /dev/video0. This results in something like:

crw-rw----+ 1 root video 81, 0 Jul  6 10:22 /dev/video0

Where the first number (81) is the device major number. Some common device major numbers:

  • 81: usb webcams
  • 188: usb to serial converters

Step 2

Add rules when you start the docker container:

  • Add a --device-cgroup-rule='c major_number:* rmw' rule for every type of device you want access to
  • Add access to udev information so docker containers can get more info on your usb devices with -v /run/udev:/run/udev:ro
  • Map the /dev volume to your docker container with -v /dev:/dev

Wrap up

So to add all usb webcams and serial2usb devices to your docker container, do:

docker run -it -v /dev:/dev --device-cgroup-rule='c 188:* rmw' --device-cgroup-rule='c 81:* rmw' ubuntu bash

--device works until your USB device gets unplugged/replugged and then it stops working. You have to use cgroup devices.allow get around it.
You could just use -v /dev:/dev but that's unsafe as it maps all the devices from your host into the container, including raw disk devices and so forth. Basically this allows the container to gain root on the host, which is usually not what you want.
Using the cgroups approach is better in that respect and works on devices that get added after the container as started.

See details here: Accessing USB Devices In Docker without using --privileged

It's a bit hard to paste, but in a nutshell, you need to get the major number for your character device and send that to cgroup:

189 is the major number of /dev/ttyUSB*, which you can get with 'ls -l'. It may be different on your system than on mine:

root@server:~# echo 'c 189:* rwm' > /sys/fs/cgroup/devices/docker/$A*/devices.allow  
(A contains the docker containerID)

Then start your container like this:

docker run -v /dev/bus:/dev/bus:ro -v /dev/serial:/dev/serial:ro -i -t --entrypoint /bin/bash debian:amd64

without doing this, any newly plugged or rebooting device after the container started, will get a new bus ID and will not be allowed access in the container.


Adding to the answers above, for those who want a quick way to use an external USB device (HDD, flash drive) working inside docker, and not using priviledged mode:

Find the devpath to your device on the host:

sudo fdisk -l

You can recognize your drive by it's capacity quite easily from the list. Copy this path (for the following example it is /dev/sda2).

Disque /dev/sda2 : 554,5 Go, 57151488 octets, 111624 secteurs
Unités : secteur de 1 × 512 = 512 octets
Taille de secteur (logique / physique) : 512 octets / 512 octets
taille d'E/S (minimale / optimale) : 512 octets / 512 octets

Mount this devpath (preferable to /media):

sudo mount <drive path> /media/<mount folder name>

You can then use this either as a param to docker run like:

docker run -it -v /media/<mount folder name>:/media/<mount folder name>

or in docker compose under volumes:

services:
  whatevermyserviceis:
    volumes:
      - /media/<mount folder name>:/media/<mount folder name>

And now when you run and enter your container, you should be able to access the drive inside the container at /media/<mount folder name>

DISCLAIMER:

  1. This will probably not work for serial devices such as webcams etc. I have only tested this for USB storage drives.
  2. If you need to reconnect and disconnect devices regularly, this method would be annoying, and also would not work unless you reset the mount path and restart the container.
  3. I used docker 17.06 + as prescribed in the docs

With current versions of Docker, you can use the --device flag to achieve what you want, without needing to give access to all USB devices.

For example, if you wanted to make only /dev/ttyUSB0 accessible within your Docker container, you could do something like:

docker run -t -i --device=/dev/ttyUSB0 ubuntu bash