ESP32 Firmware Development with Docker Sandboxes

mfranzon1 pts0 comments

ESP32 Firmware Development with Docker Sandboxes | Docker

Skip to content

Search

Sign In

Toggle menu

Docker Captain

Reproducible ESP32 Firmware Development with Docker and Docker Sandboxes

Posted Aug 14, 2026

Marco Franzon

Firmware development has always been challenging: mismatched toolchains, “it works on my machine” builds, and the tension between maintaining legacy products and shipping new features. In this article we explore how you can use Docker and Docker sandboxes to ease firmware development, especially for ESP32 projects. Nowadays, teams end up supporting multiple hardware revisions, several ESP-IDF releases, and long-term customer deployments, all while iterating on new capabilities like Wi-Fi 6, Matter, or power optimizations.

The official espressif/idf Docker image solves the reproducibility problem. Docker Sandboxes (the sbx CLI) solve a newer one: letting AI coding agents work on your firmware at full speed without giving them the keys to your laptop. This article walks through a practical workflow that combines both: clean builds, parallel environments for new and legacy firmware, and safe unsupervised AI sessions.

Part 1: The Baseline – Building with the Official Image

The espressif/idf image ships a complete, pinned ESP-IDF installation: the framework itself, the Xtensa/RISC-V toolchains, Python environment, CMake, ninja, everything. A build needs one command:

docker run --rm -v $PWD:/project -w /project \<br>-u $UID -e HOME=/tmp \<br>espressif/idf:release-v5.4 idf.py build

A few details worth understanding rather than cargo-culting:

-u $UID -e HOME=/tmp makes the container run as your user, so build artifacts in build/ aren’t owned by root. HOME=/tmp gives the IDF tools a writable home for their caches.

Pin your tag. latest tracks the master branch and will break you eventually. vX.Y tags are fixed releases; release-vX.Y tags track the release branch and receive bugfixes. For products in maintenance, exact vX.Y.Z tags are the safest; for active development, release-vX.Y is a good balance.

If your mounted project is owned by a different user than the one in the container, Git will complain about “dubious ownership”. The image supports -e IDF_GIT_SAFE_DIR='/project' to whitelist the path (use : to separate multiple paths).

Enable the compiler cache with -e IDF_CCACHE_ENABLE=1 and persist it across runs by mounting a volume for it. Full rebuilds of a mid-size project drop from minutes to seconds.

Flashing and monitoring

On Linux , pass the serial device through:

docker run --rm -it \<br>--device=/dev/ttyUSB0 \<br>--group-add $(getent group dialout | cut -d: -f3) \<br>-v $PWD:/project -w /project \<br>-u $UID -e HOME=/tmp \<br>espressif/idf:release-v5.4 idf.py flash monitor

The --group-add is needed because you’re running as $UID, not root, and the device node belongs to dialout.

On macOS and Windows , Docker Desktop cannot pass USB devices into containers. The clean workaround is a network serial bridge using RFC2217, which esptool supports natively. On the host:

pip install esptool<br>esp_rfc2217_server -p 4000 /dev/cu.usbserial-1420

Inside the container, point idf.py at the network port:

idf.py --port 'rfc2217://host.docker.internal:4000?ign_set_control' flash monitor

This looks like a hack but it’s actually a feature: once the serial port is a network endpoint, anything can reach it. Containers, CI runners, and (as we’ll see) sandboxed AI agents. Keep this trick in mind; it’s the linchpin of Part 3.

Hide it behind a Makefile

Nobody should type these commands twice. A small Makefile keeps the interface stable even if the plumbing changes:

IDF_IMAGE ?= espressif/idf:release-v5.4<br>PORT ?= /dev/ttyUSB0

DOCKER_RUN = docker run --rm -it \<br>--device=$(PORT) \<br>--group-add $(shell getent group dialout | cut -d: -f3) \<br>-v $(PWD):/project -w /project \<br>-v idf-ccache:/ccache -e CCACHE_DIR=/ccache -e IDF_CCACHE_ENABLE=1 \<br>-u $(shell id -u) -e HOME=/tmp -e IDF_GIT_SAFE_DIR=/project \<br>$(IDF_IMAGE)

build:<br>$(DOCKER_RUN) idf.py build

flash:<br>$(DOCKER_RUN) idf.py flash

monitor:<br>$(DOCKER_RUN) idf.py monitor

menuconfig:<br>$(DOCKER_RUN) idf.py menuconfig

shell:<br>$(DOCKER_RUN) bash

Now make build works identically for every developer and in CI, and switching IDF versions is make build IDF_IMAGE=espressif/idf:release-v5.3.

Part 2: Parallel Environments – New Features and Legacy, Side by Side

This is where the container approach stops being merely convenient and starts changing how you work. Because each container is fully isolated, you can run two different IDF versions against two different boards at the same time, on the same machine.

# Terminal 1 - new feature branch, IDF 5.4, experimental board<br>docker run --rm -it --device=/dev/esp32-experimental \<br>-v $PWD/new-feature:/project -w /project \<br>-u $UID -e HOME=/tmp \<br>espressif/idf:release-v5.4

# Terminal 2 - legacy firmware, IDF 5.3, production board<br>docker run --rm -it --device=/dev/esp32-production \<br>-v $PWD/legacy:/project -w /project \<br>-u $UID...

docker project firmware build release espressif

Related Articles