How do you give a Docker container access to one host device such as /dev/ttyACM0, and why is --privileged the wrong answer?
answer
- Two barriers, not one
- Visible is not the same as permitted
- The node plus the allow-list
- host:container:rwm
- One key, not the master key
basics
~20 sUse docker run --device /dev/ttyACM0:/dev/ttyACM0. Docker creates that one device node inside the container and permits it in the container's device allow-list. --privileged works only by unlocking every host device at once, which is far too broad.
solid answer
~40 sA container's /dev is a small tmpfs the daemon populates with a handful of standard nodes, so host devices are simply absent. `docker run --device <host>[:<container>[:<perms>]]` fixes both halves of the problem: it creates the device node inside the container and adds that major:minor pair to the container's cgroup device allow-list. Permissions are any of `r`, `w` and `m` (mknod), defaulting to `rwm`, and you can rename the node on the way in, e.g. `--device /dev/ttyACM0:/dev/token0:rw`. A plain bind mount (`-v /dev/ttyACM0:/dev/ttyACM0`) is the classic trap: the node appears, but the allow-list was never updated, so `open()` fails with EPERM. `--privileged` is the lazy alternative because it allows *every* device plus the full capability set, turning a one-device requirement into host-level access if the process is ever compromised.
code
bash · 8 lines# One device, renamed on the way in, with mknod withheld
docker run -d --name pdf-signer \
--device /dev/ttyACM0:/dev/token0:rw \
--group-add 20 --user 10001:10001 \
pdf-signer:4.7
# Confirm what the container actually got
docker inspect --format '{{json .HostConfig.Devices}}' pdf-signergo deeper
Remember the flag and its shape: --device host-path:container-path:permissions, and that a -v bind mount of a device node is not a substitute. Being able to say why --privileged is too broad is enough at this level.
Explain the two separate barriers — the node must exist in the container's /dev and the cgroup device controller must permit that major:minor — and what each of r, w and m grants.
Show the diagnosis path: distinguish EPERM from EACCES, know that devices are fixed at create time, and handle hotplug with a --device-cgroup-rule plus a bind mount rather than escalating to --privileged.
Own the policy: which workloads may request devices at all, how --privileged is kept out of the fleet, and how narrow device grants are reviewed and audited when a vendor SDK insists it needs more than one node.
### What a device is, and why the container cannot see it On Linux a device is reached through a *device node*: a special file in `/dev` carrying a type (`c` for character, `b` for block) and a major/minor number pair that tells the kernel which driver to call. Opening `/dev/ttyACM0` is how a process talks to a USB serial adapter or a smartcard reader. A container gets its own mount namespace, and the Docker daemon builds its `/dev` from scratch: a small tmpfs holding `null`, `zero`, `full`, `random`, `urandom`, `tty`, plus `/dev/console`, `/dev/pts` and `/dev/shm`. Nothing from the host is inherited. On top of that the container runs under the kernel's device controller, which by default permits only that same minimal set. So there are **two independent barriers**, and every confused bug report about device access comes from fixing only one of them. ### `--device` fixes both barriers ``` docker run --device /dev/ttyACM0:/dev/ttyACM0:rw ... ``` The argument is `host-path[:container-path[:permissions]]`. Permissions are drawn from `r` (read), `w` (write) and `m` (mknod, i.e. create further nodes with that major/minor); the default is `rwm`. The container path may differ from the host path, which is useful when the image expects a fixed name but the host enumerates the hardware differently — `--device /dev/ttyACM1:/dev/token0` presents the second reader as the only one the app knows about. What the daemon does with it: it records the request in `HostConfig.Devices` (visible with `docker inspect --format '{{json .HostConfig.Devices}}' <container>`), creates the node inside the container's `/dev`, and writes an allow rule for that major:minor into the container's cgroup device controller. Devices are fixed at create time — there is no `docker update --device`, so changing the set means recreating the container. ### The bind-mount trap `-v /dev/ttyACM0:/dev/ttyACM0` looks equivalent and is not. The bind mount makes the node *visible* — `ls -l` shows it with the right major/minor — but the device controller still denies it, so the application fails with `EPERM` / `Operation not permitted` on `open()`. "The file is right there and I still cannot read it" is the fingerprint of this mistake. ### Hotplug and dynamically numbered devices `--device` is evaluated once, when the container is created. Unplug a USB token and plug it back in and the host may hand it a different node, which the running container will never see. The standard workaround is a wildcard rule plus a bind mount of the USB tree: `--device-cgroup-rule 'c 189:* rmw'` (189 is the major used for USB character devices on most Linux hosts) permits the whole class, while `-v /dev/bus/usb:/dev/bus/usb` keeps newly appearing nodes visible. That is broader than one device but still enormously narrower than `--privileged`. ### Permissions inside the container The node keeps the host's ownership and mode — a reader is often `root:dialout 0660`. If the image runs as a non-root `USER`, the cgroup will allow the device and the *file permissions* will still refuse it, giving `EACCES` rather than `EPERM`. The fix is `--group-add <numeric host GID>`: numeric, because the container's `/etc/group` usually has no `dialout` entry to resolve the name against. ### Worked example A PDF-signing service — a Node.js API on an alpine base, a 1.7 GB image — signs documents with a USB hardware token exposed at `/dev/ttyACM0`, owned by `root:dialout` with GID 20 on the host: ``` docker run -d --name pdf-signer \ --device /dev/ttyACM0:/dev/ttyACM0:rw \ --group-add 20 --user 10001:10001 \ pdf-signer:4.7 ``` One device, one group, a non-root user. Compare that with `--privileged`, which would hand the same process `/dev/sda` as well. ### Why `--privileged` is the lazy answer `--privileged` sets the device allow-list to "allow everything", exposes the host's devices, grants the full capability set and relaxes the other confinement layers a container normally runs under. It does make the reader work — and it also lets the process open the host's root block device and rewrite the filesystem, so a remote-code-execution bug in a PDF parser becomes host compromise instead of container compromise. In a design review, `--privileged` where a single `--device` would do is a defect, not a shortcut. Reach for the narrowest of `--device`, then `--device-cgroup-rule`, and only then argue for anything wider.
- What is the difference between the EPERM and EACCES a container can get when opening a passed-through device?EPERM (`Operation not permitted`) means the kernel's device controller refused the major:minor — the device was never allowed for that container, typically because it was bind-mounted instead of passed with `--device`. EACCES (`Permission denied`) means the allow-list is fine but ordinary file permissions refused: the node is `root:dialout 0660` and the container's user is neither. The first is fixed with `--device`, the second with `--group-add` or a host udev rule.
- Can you add a device to a container that is already running?No. `HostConfig.Devices` is fixed at create time and `docker update` does not cover devices, so the answer is to recreate the container with the extra `--device`. If devices genuinely come and go at runtime, create the container with a `--device-cgroup-rule` covering the whole class plus a bind mount of the relevant `/dev` subtree, so new nodes are both permitted and visible without a restart.
- What does the `m` permission in `--device /dev/sdb:/dev/sdb:rwm` actually grant?`m` allows `mknod` for that major/minor — the container may create further device nodes pointing at the same driver. Most applications only need `r` and/or `w`, so dropping `m` costs nothing and removes a way for a process inside the container to re-materialise a node it should not have. It is the cheapest tightening available on the flag.
--device is a key to one room; --privileged is the master key to the building, handed over because one door was locked.
saying these in an interview costs you the question
- Says `-v /dev/ttyUSB0:/dev/ttyUSB0` is the way to pass a device
- Reaches for `--privileged` as the normal solution to any device problem
- Thinks `--privileged` only affects devices and nothing else
- Believes a device can be attached to an already-running container
- Assumes root inside the container makes host file permissions irrelevant
- Cannot distinguish `Operation not permitted` from `Permission denied`