A board plugged into the Mac over USB — an ESP32, an Arduino, anything that
shows up as /dev/cu.* — cannot be handed to the container. The toolbox reaches
it the way mobile.md reaches a phone: a small
server on the host owns the device, and the tools in the container talk to it
over TCP.
--device does not workDocker Desktop runs every container inside a Linux VM. The Mac's USB devices
belong to macOS, and the VM never sees them, so there is no /dev/ttyUSB0 to
map. docker run --device /dev/cu.usbserial-… fails, and so would anything the
toolbox could add to the container. Nothing in the toolbox needs to change for
the approach below: the container already reaches the host as
host.docker.internal.
RFC 2217 is serial over telnet. Unlike a raw TCP pipe it also carries the baud rate and the DTR/RTS control lines, and those lines are what puts an ESP32 into its bootloader and resets it. esptool ships a server for it.
On the host:
brew install esptool # brings esp_rfc2217_server with it
ls /dev/cu.* # find the board
esp_rfc2217_server -v /dev/cu.usbserial-XXXX
It listens on its default port, 2217, until you stop it with Ctrl-C. Restart it whenever you reconnect the board. While it runs it holds the serial port, so nothing else on the host can open the board at the same time.
Address already in use means another process holds the port, and on Docker
Desktop that is often a container publishing it: docker ps --format '\t' names it. Pick another port with -p <port> and use
the same one in the URL below.
The tools need no install in the image: uvx runs them on demand.
P='rfc2217://host.docker.internal:2217?ign_set_control'
uvx esptool --port "$P" chip-id
uvx esptool --port "$P" write-flash 0x1000 firmware.bin
esptool prints Failed to get VID/PID on every reset. That is harmless: a
network URL has no USB identity, and esptool falls back to the standard reset
sequence.
Any tool built on pyserial takes the same URL, because pyserial opens it with
serial_for_url. A tool that insists on a device path does not.
Expect every new connection to reset the board: the firmware starts over each
time a tool opens the URL. esptool does not care, it resets the board itself.
An interactive tool can. A MicroPython board whose main.py goes into deep
sleep straight after boot turns its UART off before mpremote gets its single
Ctrl-C through, and mpremote fails with could not enter raw repl. Either
give main.py a few seconds before it sleeps, or have the tool send Ctrl-C
repeatedly until the >>> prompt shows.
The server has no authentication, and it listens on every interface. Anyone on the same network can connect to it, then flash the board or read what it prints. It accepts one client at a time. Stop it when you are not using it, and do not leave it running on a shared or public network.