A clean, isolated ESP-IDF environment built for professional firmware teams. It removes setup pain, dependency conflicts, and version issues. Drop in your project, choose the IDF version, and everything just works.
- No Python Conflicts: ESP-IDF runs in an isolated Docker container
- Version Locked: Pin specific ESP-IDF versions per project
- Cross-Platform: Works on Windows and Linux (macOS NOT tested yet)
- USB Passthrough: Full hardware debugging and flashing support
- Production Ready: Includes safe defaults and best practices
- ESP32 development board (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, etc.)
- USB programmer/debugger (often built into dev boards or ESPPROG)
This whole system was tested with the original ESPProg programmer and ESP32-S3-WROOM-1 microcontroller.
It SHOULD easily support any other programming tool on USB that is supported by ESP-IDF framework.
- Docker Desktop (Windows/Mac) or Docker Engine (Linux)
- WSL2 (Windows only)
- USBIPD (Windows only)
- docker compose (v2) (Linux)
- Git
On Linux, you need to install Docker and docker-compose (version 2!).
That docker-compose MUST be version 2, not the legacy version 1.
To test if you have installed the right docker compose version, try running:
docker compose(notice there's NO "-" dash between the words!)
"docker compose" = v2 "docker-compose" = v1
You need the command without the dash installed, that's the docker compose v2.
On Windows, you'll need a few more things...
Install Docker Desktop - this will also install the latest docker compose (v2).
IMPORTANT!
Windows ALSO requires bridging USB devices from the host (your machine) to Docker.
By default, when you start a Docker container on Windows, the container does NOT see your USB devices connected to your host computer.
If you plug your ESPPROG to your PC, it won't suddenly appear INSIDE our Docker container and it cannot be used just like that to communicate with your programmer.
To make it appear inside, so that idf.py and other tools can use your programmer, we must do a few steps...
To be able to connect your USB device from Windows to the Docker container, we need a special tool for Windows - USBIPD.
Install the USBIPD via Windows' package manager:
winget install --interactive --exact dorssel.usbipd-winThis MIGHT require admin privileges.
In case the setup hangs or fails, try to rebooting your computer and running again.
Caution
After installing USBIPD, you MUST reboot your computer. Otherwise the tool might NOT get in PATH and our scripts will NOT be able to find the usbipd tool!
With this tool installed and PC restarted, you are ready to rock 'n roll on Windows!
# Extract the package
unzip lysos-workspace-v*.zip
# Navigate to the extracted directory
cd lysos-workspace-v*
# Copy environment template
cp .env.example .env
# IMPORTANT!
# Edit .env - some parameters inside MUST BE set before continuing!On Linux, your USB programmer device will be immediately available as /dev/ttyUSB0, /dev/ttyUSB1 or similar (2, 3, etc...)
You can see your USB devices available by going to /dev on your Linux host machine and checking which ones are available like this:
cd /dev
llOpen your .env file and set the ESPPROG variable to /dev/ttyUSB0 or similar.
Important
If you are setting USB port number HIGHER THAN 1 - like 2, 3, etc... see the "Multiple Serial Ports" section down below as well - MANDATORY!
Some programmers like ESPProg have TWO ports...
Pick USB1 for start and if it won't work, we switch to USB0...
On Windows, once you have the container running, we must bridge the USB device to the Docker container.
We use that USBIPD tools we installed previously for that.
Go to "scripts" directory:
cd scriptsNow.
Each device (your USB programmer) in a SPECIFIC USB port must be BOUND to the Docker container only ONCE.
It's a one-time setup. After that, you don't need to bind anymore.
So you plug in your USB programmer to ANY usb port.
Then we list the USB devices available.
# List connected USB devices
cd scripts
.\usb-list.ps1Example output:
BUSID VID:PID DEVICE STATE
1-2 0403:6010 USB Serial Converter A Not shared
"Not shared" means it has NOT been bound to the Docker yet.
Note the BUSID (e.g., 1-2) of your ESP32 programmer.
Warning
If you cannot see your device in the usb list output, try to connecting it to an older USB port. On my PC, ESPProg is NOT listed in the list when connected to a USB3.1 3-meter-long cable, but it does work when I connect it WITHOUT the cable. If not listed, try removing long cables or use an older USB port.
# Share device with Docker
.\usb-bind.ps1 --busid 1-2This will configure the device so that it can be bridged into the Docker.
This is a one time setup step per device per USB port and REQUIRES ADMIN PRIVILEGES - you MUST a new PowerShell terminal with Administrator rights.
Next.
If you switch your programmer to ANOTHER USB port and run list again, it will show as a DIFFERENT "device" with a DIFFERENT "BUSID".
You will have to bind that one as well.
If you're working on a laptop with say 2 USB ports, I recommend plugging the device into one after another and doing this bind FOR BOTH - so that you can use any port you want later.
After completing these "BIND" operations, YOU MUST restart your Docker container to pick up the new USB ports.
If you DON'T, you won't see them inside Docker.
cd ..
docker compose restartThis will make the USB device available inside Docker AND - UNAVAILABLE in Windows!
The access is EXCLUSIVE, so it's either inside Windows, or inside Docker, never in both.
This "ATTACH" command is required EVERY TIME you plug the device in.
If you unplug your programmer and the re-plug, you MUST run it again, so that it once again becomes available in Docker.
# Attach to Docker (required each time you plug in the device)
.\usb-attach.ps1 --busid 1-2
# After attaching, restart the container to pick up the device
# (might work right away without restart - so just to be sure)
docker compose restartNOW - and this is IMPORTANT!
You go back and open your .env file...
And set the USB_BUSID to your value like
USB_BUSID=1-2
or...
USB_BUSID=3-1
After this - we need to refresh the container again to pick up the changes in .env.
docker compose up -dAnd... That's it!
These are the only rules...
Every time you plug-in - run ATTACH.
Running DETACH is optional - the device is detached when you remove it from your USB port OR - if you need the device back in Windows, DETACH so that is it removed from Docker and made available in Windows.
Running BIND - only once for each new device in a new USB port.
Scripts Included:
# Windows PowerShell scripts in ./scripts/
.\scripts\usb-list.ps1 # List USB devices
.\scripts\usb-bind.ps1 # Bind device to Docker (needed only once per device x usb port)
.\scripts\usb-attach.ps1 # Attach device to Docker (needed after each plug in)
.\scripts\usb-detach.ps1 # Detach device from Docker# Copy your entire ESP-IDF project folder into project-root-folder
cp -r /path/to/your/my-esp32-project ./project-root-folder/
# Result structure:
# project-root-folder/
# └── my-esp32-project/ ← your project
# ├── main/
# ├── CMakeLists.txt
# └── ...
# Edit .env to set your project folder name
nano .env # or use your favorite editor# Build and start the container
docker compose up -dGo to the scripts folder to use a shortcut script run.ps1 or run.sh that will make running tools super easy:
cd scripts
# Configure target (ESP32-S3 by default)
./run.sh idf.py set-target esp32s3
# Build the project
./run.sh idf.py build
# Flash to device (after USB setup - see below)
./run.sh idf.py flash
# Monitor serial output
./run.sh idf.py monitorcd scripts
# Configure target (ESP32-S3 by default)
.\run.ps1 idf.py set-target esp32s3
# Build the project
.\run.ps1 idf.py build
# Flash to device (after USB setup - see below)
.\run.ps1 idf.py flash
# Monitor serial output
.\run.ps1 idf.py monitorPress Ctrl+] to exit the monitor.
lysos-workspace-v*/
├── docker-compose.yml # Docker orchestration
├── Dockerfile # ESP-IDF container definition
├── .env.example # Environment variables template
├── README.md # This file
├── scripts/ # Helper scripts
│ ├── usb-*.ps1 # Windows USB management
│ ├── run.ps1 # Windows runner
│ └── run.sh # Linux/macOS runner
└── project-root-folder/ # COPY YOUR PROJECT FOLDER HERE
└── (your-project-name)/ # Your ESP-IDF project folder
├── main/
├── CMakeLists.txt
└── ...
The project-root-folder is where you copy your entire ESP-IDF project folder (not just the files, but the whole folder). This keeps Docker configuration separate from your code and allows proper container naming.
Structure Example:
If your project is called remote-node-zero:
project-root-folder/
└── remote-node-zero/ ← your project folder
├── main/
│ └── main.c
├── CMakeLists.txt
└── ...
Setup:
- Copy your project:
cp -r /path/to/remote-node-zero ./project-root-folder/ - Edit
.envand setPROJECT_NAME=remote-node-zero - Docker will mount
./project-root-folder/remote-node-zeroas/workspace
# Start container
docker compose up -d
# Stop container
docker compose down
# Restart container (needed after USB attach)
docker compose restart
# Enter container (not really needed)
docker compose exec esp-idf bash
# View container logs
docker compose logs -fcd scripts
# One-off commands (non-interactive)
./run.sh idf.py build
./run.sh idf.py --versionThe run.sh (for Linux) or run.ps1 (for Windows) is a shortcut script to run tools that are inside the container.
I recommend using the shortcut above ^^^ with the run.sh/ps1 script.
You first need to enter the container and then you can run ESP-IDF commands:
docker compose exec esp-idf bash
# Set target chip
idf.py set-target esp32s3
# Configure project (menuconfig TUI)
idf.py menuconfig
# Build project
idf.py build
# Flash to device
idf.py flash
# Monitor serial output
idf.py monitor
# Flash and monitor in one command
idf.py flash monitor
# Clean build
idf.py fullclean
# Check ESP-IDF version
idf.py --versionThe .env file controls build and runtime configuration.
Every time you make ANY change in the .env file, you MUST run:
docker compose down
docker compose up -d --buildWindows:
- Run
usbipd listto verify device is attached (scripts/usb-list.ps1) - Restart container:
docker compose restart - Check WSL2 has device:
wsl ls -l /dev/ttyUSB*
Linux
- Check permissions:
ls -l /dev/ttyUSB0
# Ensure no other program is using the port
# On Windows, detach and reattach USB device
.\scripts\usb-detach.ps1
.\scripts\usb-attach.ps1# Check Docker is running
docker ps
# View error logs
docker compose logs
# Rebuild container
docker compose down
docker compose up -d --build --force-recreateCopy your entire project folder into project-root-folder/:
# Copy your project folder (not just its contents!)
cp -r /path/to/my-esp32-project ./project-root-folder/
# Update .env with your project folder name
echo "PROJECT_NAME=my-esp32-project" >> .envAlternative: Use a symlink
# Create a symlink to your project
ln -s /path/to/my-esp32-project ./project-root-folder/my-esp32-project
# Update .env
echo "PROJECT_NAME=my-esp32-project" >> .envEdit docker-compose.yml to add more devices:
devices:
- /dev/ttyUSB0:/dev/ttyUSB0
- /dev/ttyUSB1:/dev/ttyUSB1
- /dev/ttyUSB2:/dev/ttyUSB2Edit sdkconfig.defaults or use idf.py menuconfig for interactive configuration.
Build cache is automatically stored in Docker volumes:
espidf-cargo-cache: Rust/Cargo cacheespidf-ccache-cache: Compiler cache
To clear cache:
docker volume rm lysos-workspace_espidf-cargo-cache
docker volume rm lysos-workspace_espidf-ccache-cache- ESP-IDF Documentation: https://docs.espressif.com/projects/esp-idf/
- ESP-IDF GitHub: https://github.com/espressif/esp-idf
- usbipd-win: https://github.com/dorssel/usbipd-win
AGPLv3 License (OSS edition).
Pro edition available under commercial license.
ESP-IDF and ESP32 are trademarks of Espressif Systems. This project is not affiliated with or endorsed by Espressif.