Docker-based development environment for embedded systems, providing containerized build environments for CI/CD workflows and local development.
| Image | Description | Documentation |
|---|---|---|
| esp-idf | ESP-IDF for every ESP32 chip, plus pytest and QEMU emulation for some of them | README |
| esp-matter | ESP-Matter SDK for Matter protocol development on ESP32 | README |
| platformio | PlatformIO with ESP32 platform support + ESP-IDF + Unity testing | README |
Every image sets /workspace as its working directory and /bin/bash as its
default command — mount your project there and pass -it for an interactive
shell:
# Pull an image - <image> is its directory name, see the table above
docker pull ghcr.io/jethome-iot/jethome-dev-<image>:latest
# Interactive development
docker run -it --rm -v $(pwd):/workspace \
ghcr.io/jethome-iot/jethome-dev-<image>:latestThe build command differs per image (idf.py for esp-idf and esp-matter, pio
for platformio). What each image contains, its supported chips, available tags,
build arguments and ready-to-run examples are documented in the image README
linked in the table above.
Build Images Locally:
# Interactive mode - select image to build and run
./scripts/build.sh
# Build specific image (tagged as 'local')
./scripts/build.sh esp-idf
./scripts/build.sh platformio
# Build and run image interactively
./scripts/build.sh -r esp-idf
./scripts/build.sh --run platformio
# Build with custom tag
IMAGE_TAG=dev ./scripts/build.sh esp-idf
# Build all images
./scripts/build.sh allThe script builds images with the local tag by default to distinguish them from registry images. Use the -r or --run flag to automatically run the image in interactive mode after a successful build. You can customize the tag using the IMAGE_TAG environment variable.
Lint Before Pushing:
./scripts/lint.shRuns the same checks as the 🧹 Lint workflow: actionlint over the workflow files,
shellcheck over the tracked *.sh files and ./scripts/check-pins.sh over the
Dockerfiles are gates, hadolint over the Dockerfiles is advisory. actionlint,
shellcheck and hadolint run as containers, so the Docker daemon has to be up; the
pin check is plain bash. The workflow itself is triggered by changes to workflows,
shell scripts and Dockerfiles.
Check What the Images Install:
./scripts/check-pins.shimages/versions.json pins what CI builds; this pins what a build installs.
Every package named on a pip install line must carry ==, and every pio
package must be name@version rather than a range — @^2.6.0 resolves again on
every rebuild just like a bare name does. apt is advisory and stays that way:
Debian rewrites its pool at every point release, so a pinned version becomes a 404
a quarter later, and in Ubuntu the only version that never disappears is the one
that predates its own security updates. A deliberate exception is a
# pin-allow: <package> - <reason> comment in the Dockerfile it applies to, and an
exception that covers nothing fails the check.
Check Versions Before a Bump:
./scripts/check-versions.shCI runs this first thing and fails the whole workflow if it does not pass. It
checks images/versions.json — the single source of truth for what CI builds —
against the Dockerfiles: the versions passed as build arguments must match the
ARG defaults a local build uses, every variant must name the versions it is built
with in its own tag, exactly one variant per image may be primary (the one that
gets latest), and an image built on another must name a base tag that base
actually publishes.
🔎 Runner Smoke Test is a separate workflow: it runs a one-minute job on every
runner pool listed in .github/actionlint.yaml and reports architecture, vCPU,
RAM and free disk. Run it after a pool is added, renamed, or granted access to
this repository — a runs-on naming a pool it cannot reach does not fail, it
queues for 24 hours.
Two ways to start it: from the Actions tab (workflow_dispatch), or by pushing a
branch named runner-probe/<anything>. The second exists because
workflow_dispatch is only offered for workflows already on the default branch,
which would leave a change to the probe itself untestable until after it merged.
It never runs on a pull request.
Test Workflows on GitHub Actions:
# Workflows run on pushes and pull requests to dev or master, but only when the
# change touches that workflow's paths:
# esp-idf.yml -> .github/workflows/esp-idf.yml, images/esp-idf/**, images/esp-matter/**
# platformio.yml -> .github/workflows/platformio.yml, images/platformio/**
# Both workflows also support workflow_dispatch (Actions tab), which ignores the
# path filters. Images are pushed to GHCR from master only.
#
# In a fork nothing runs at all: the build jobs require the jethome-iot owner,
# and the runner pools they target are not reachable from a fork - a job asking
# for one would queue for 24 hours rather than fail. Build in a fork with
# ./scripts/build.sh instead. An image built FROM another image of this repo
# (today esp-matter) is also skipped on dev and on pull requests: it chains off
# the base image's manifest job, which runs on master only - see the note below.
git checkout dev
git push origin dev
# Monitor workflow progress
gh run list --workflow="🐳 ESP-IDF Docker Image" --limit 5
gh run watch
# View logs
gh run view <run-id> --logRequires GitHub CLI to be installed.
There is no local workflow runner: workflow changes are validated by pushing the
branch and reading the PR's checks. To iterate on a Dockerfile itself, use
./scripts/build.sh — it builds the same context, but as a plain docker build
for your host architecture only and with the Dockerfile's own ARG defaults
instead of the versions CI passes in, so a green local build is not a green CI
run.
Note: every image is build-validated on pull requests and on dev, including
one built FROM another image of this repo, and on the base that same run
produced — so a change touching both images is checked as the pair it will become.
This works because the ESP-IDF build job pushes by digest on every run — it is
the only image another one is built from, so it is the only one that needs to.
Those pushes carry no tag, and only master ever writes latest or a version tag.
The untagged blobs a pull request leaves in GHCR accumulate and want an occasional
cleanup. PlatformIO, which nothing builds on, still uploads nothing outside
master.
Documentation-only changes trigger nothing: !images/**/*.md is excluded from both
workflows' path filters.
cd images/platformio
docker build -t jethome-dev-platformio:local .Note: Locally built images use the local tag by default to distinguish them from registry images tagged with latest.
jethome-dev/
├── .github/
│ ├── actionlint.yaml # Larger-runner pools, read by the linter and the probe
│ └── workflows/ # GitHub Actions workflows
│ ├── esp-idf.yml # ESP-IDF and ESP-Matter image workflows
│ ├── platformio.yml # PlatformIO image workflow
│ ├── lint.yml # actionlint + shellcheck (gates), hadolint (advisory)
│ └── runner-smoke.yml # Reports what each runner pool actually is
├── images/
│ ├── versions.json # Single source of truth for the versions CI builds
│ ├── esp-idf/ # ESP-IDF development image
│ │ ├── Dockerfile # Image definition
│ │ └── README.md # Detailed documentation
│ ├── esp-matter/ # ESP-Matter development image
│ │ ├── Dockerfile # Image definition
│ │ ├── entrypoint.sh # Activates ESP-IDF + ESP-Matter env on start
│ │ └── README.md # Detailed documentation
│ └── platformio/ # PlatformIO development image
│ ├── Dockerfile # Image definition
│ ├── README.md # Detailed documentation
│ └── pio_project/ # Stub project for the disabled pre-build step
├── scripts/
│ ├── build.sh # Local image build helper
│ ├── lint.sh # Runs the same linters as CI, locally
│ ├── versions-matrix.sh # Turns versions.json into the CI matrices
│ ├── check-versions.sh # Enforces versions.json against the Dockerfiles
│ └── update-matter-ref.sh # Reports/advances the pinned ESP-Matter commits
├── CLAUDE.md # Repository conventions, loaded by Claude Code
├── LICENSE
└── README.md
Each image directory is self-contained: the workflows build it with
context: images/<name>, so a Dockerfile can only COPY files from inside its own
directory.
Every image is published to GitHub Container Registry (GHCR) as its own package,
named after its directory under images/:
ghcr.io/jethome-iot/jethome-dev-<image>.
See the image's own README, linked in Current Images, for its available tags and usage examples.
- CI/CD: Automated firmware builds in GitHub Actions, GitLab CI
- Team Development: Consistent build environment across team
- Multi-platform: Build for multiple ESP32 variants from a single container
- Testing: Native platform for unit tests with Unity framework
- ✅ Reproducible builds across all machines — images are pinned by version tags
- ✅ Multi-architecture: every image is published for
linux/amd64andlinux/arm64 - ✅ Multiple ESP32 chip variants supported by a single image
- ✅ Images kept minimal — toolchains that are not needed at build time download on first use
- ✅ CI/CD optimized (no USB/serial dependencies)
MIT License - see LICENSE file for details.
Note: More development images may be added in the future.