Skip to content
Public template

About

Docker overlay + Windows/WSL2 scripts that let cargo run flash and monitor a real ESP32 board from a container -- built on espressif/idf-rust.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

esp32-rust-devbox

A reusable Docker setup that lets cargo run build, flash, and monitor a physical ESP32-family board over USB from inside a container on Windows (Docker Desktop's WSL2 backend) -- something no existing published image or official tooling currently does end-to-end. Extracted from a working setup built and verified against real hardware on a XIAO ESP32-S3 board + TRMNL ePaper display project, then generalized here for reuse across future ESP32+Rust projects with different chips/displays.

Why this exists

espressif/idf-rust (official, maintained by the esp-rs team) already provides the hard part: the Rust+xtensa/riscv toolchain, ESP-IDF, and even espflash and web-flash pre-installed, per chip target. This repo doesn't reinvent any of that -- it's a thin overlay (Dockerfile) plus two scripts that solve the one thing left unsolved: getting a container running on Windows real access to the board's USB serial port.

The official answer to that problem, used by esp-rs's own devcontainer templates, is esp-web-flash-server: build in the container, then manually open a browser tab, click connect, pick the COM port via the WebSerial permission popup, and click install -- every single flash. Monitoring the device's serial output is a completely separate step/tool on top of that (espflash monitor), since web-flash only flashes. There's also a known open issue (espflash#641) describing espflash-over-WSL2-via-usbipd breakage, with no documented settled fix.

What's here instead: real USB device passthrough into the container, so cargo run builds, flashes, and monitors in one command, the same as it would on bare native hardware -- the only extra step is a one-time per-Docker-Desktop-session PowerShell script.

Quick start

  1. Copy into your project: Dockerfile, scripts/attach-esp32-usb.ps1, scripts/container-flash-runner.sh, and merge the dev service from docker-compose.snippet.yml into your own docker-compose.yml.
  2. Add examples/cargo-config-runner-snippet.toml's content to your project's .cargo/config.toml, with your actual target triple.
  3. In docker-compose.yml, set IDF_RUST_TAG (see available tags) and CARGO_TARGET_<...>_RUNNER to match your chip.
  4. Every Docker Desktop session, board plugged in:
    .\scripts\attach-esp32-usb.ps1
  5. docker compose exec dev bash, then cargo run -- builds, flashes, and monitors.

The Windows/WSL2/usbipd-win story

Docker Desktop's containers run inside a hidden WSL2 distro (docker-desktop), separate from any user-visible Ubuntu/etc distro -- containers don't see host USB devices directly. usbipd-win bridges that gap by attaching a USB device into a specific WSL2 distro's kernel instance.

attach-esp32-usb.ps1 automates the whole flow:

usbipd list                                          # finds the board by VID (default 303a, Espressif)
usbipd bind --busid <BUSID>                          # one-time per physical USB port
usbipd attach --wsl docker-desktop --busid <BUSID> --auto-attach

--auto-attach (not a plain attach) matters because most ESP32 boards reset and re-enumerate partway through every flash (toggling into their USB bootloader), which drops and re-establishes the USB connection -- a one-shot attach wouldn't survive that. The attach command is a foreground watcher; the script leaves it running in its own window for the session. Re-run the script after a host reboot, a Docker Desktop restart, or moving the board to a different USB port (new BUSID, needs re-binding).

One more per-session step the script also does: the docker-desktop WSL distro has no udev, so nothing ever triggers the kernel's usual hotplug module autoload. The driver that turns the attached USB device into a /dev/ttyACM0 (or /dev/ttyUSB0) node has to be loaded by hand:

wsl -d docker-desktop -- modprobe cdc_acm

Confirmed empirically: without this, lsusb inside the distro shows the board fine, but no /dev/tty* node ever appears, and espflash reports "No serial ports could be detected". Once loaded, the module stays loaded for the life of that WSL2 VM -- only a fresh Docker Desktop start needs this again, not every flash or replug.

Native USB vs. an external bridge chip

Boards with the chip's native USB (most ESP32-S2/S3/C3/C6 dev kits, including the XIAO ESP32-S3 this was built against) enumerate as VID 303a (Espressif) and need the cdc_acm kernel module. Older/other boards instead expose an external USB-serial bridge chip -- Silicon Labs CP210x (VID 10c4) or WCH CH340 (VID 1a86) are the common ones. Pass -Vid/-KernelModule to attach-esp32-usb.ps1 to match (cp210x/ch341 respectively); check usbipd list with the board plugged in if you're not sure which one you have.

Why sudo, not a dialout group

The base image's default user (esp) has no password, so plain sudo would fail auth outright -- hence the narrowly-scoped, passwordless sudoers rule in the Dockerfile (only chmod 666 on /dev/ttyACM*//dev/ttyUSB*, nothing broader). A dialout/uucp group, the usual fix on a real Linux host, doesn't work here: that group assignment is normally applied by a udev rule, and the docker-desktop distro has no udev running to apply it. Every re-enumeration resets the device node back to a restrictive default, so scripts/container-flash-runner.sh re-chmods it before every flash -- this is expected, permanent behavior of this setup, not a one-time fix.

Why privileged+/dev, not a devices: list

privileged: true lifts the container's device-cgroup allow-list, and bind-mounting the host's live /dev (rather than a static devices: entry naming one specific node) means whatever ttyACM*/ttyUSB* node currently exists is visible without a container restart -- necessary since the board's own mid-flash reset means the node can disappear and reappear. This does give the dev container broad access to the WSL distro's whole device namespace, not just the serial port -- a deliberate trade-off, reasonable for a single-developer local dev container, not a shared/production one.

Troubleshooting

  • espflash reports no serial ports found: check usbipd list shows "Attached", then check wsl -d docker-desktop -- lsusb shows the board, then check the cdc_acm modprobe step ran.
  • Permission denied opening the serial port: the chmod in container-flash-runner.sh runs before every flash automatically -- if you're testing manually instead, run sudo chmod 666 /dev/ttyACM0 yourself first.
  • Device disappeared mid-flash and never came back: check the attach-esp32-usb.ps1-spawned watcher window is still open and didn't error out: --auto-attach needs that window alive for the whole session.
  • docker-desktop distro not found: run wsl -l -v to confirm the actual running distro name (docker-desktop, not docker-desktop-data) -- pass -WslDistribution if it's ever different on your machine.

License

MIT -- see LICENSE.

About

Docker overlay + Windows/WSL2 scripts that let cargo run flash and monitor a real ESP32 board from a container -- built on espressif/idf-rust.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages