Skip to content

support for building inside docker#5630

Open
ondras wants to merge 3 commits into
wled:mainfrom
ondras:docker-build
Open

support for building inside docker#5630
ondras wants to merge 3 commits into
wled:mainfrom
ondras:docker-build

Conversation

@ondras
Copy link
Copy Markdown

@ondras ondras commented May 19, 2026

Added a Dockerfile to create an image with building infrastructure.
Added a compose.yaml to build+run that image in order to create a binary in a clean environment.

Summary by CodeRabbit

  • Chores
    • Added a containerized build environment to provide consistent, reproducible builds across machines.
    • Added a compose-based service to run the build and output artifacts to a local folder, with a configurable build target.
    • Excluded common VCS/editor files, logs, temporary files, dependencies, and build output from the build context to speed up image builds and reduce image size.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Walkthrough

Adds containerized build support: .dockerignore to reduce build context, a Dockerfile that installs Node/Python and project dependencies, and compose.yaml defining a wled-build service that runs npm run build and pio run with a mounted build_output.

Changes

Docker Build Environment

Layer / File(s) Summary
Docker image definition and context optimization
.dockerignore, Dockerfile
.dockerignore excludes VCS/editor folders, *.log, *.tmp, node_modules/, and build_output/. Dockerfile creates an Ubuntu-based build image, installs nodejs/npm, git, ca-certificates, python3-pip, copies the repo, runs npm ci, installs Python requirements from requirements.txt with pip flags, and sets CMD ["bash"].
Build service orchestration
compose.yaml
compose.yaml adds the wled-build service that builds the image from ., mounts ./build_output to /workdir/build_output, provides PIO_ENV defaulting to esp32dev, and runs npm run build then pio run -e $PIO_ENV via bash -c.

🎯 2 (Simple) | ⏱️ ~8 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly reflects the main purpose of the changeset: adding Docker support for building the project with new Dockerfile, compose.yaml, and .dockerignore files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
Dockerfile (1)

1-16: ⚖️ Poor tradeoff

Consider adding a non-root USER directive for defense-in-depth.

Static analysis flagged that the container runs as root. While this is often acceptable for build-only containers that don't run services, adding a non-root user provides defense-in-depth security. However, given that this is strictly a build container and not a runtime service, this is optional.

🛡️ Example of adding a non-root user
 FROM ubuntu:latest
 
 RUN apt-get update \
     && apt-get install -y --no-install-recommends nodejs npm git ca-certificates python3-pip \
     && rm -rf /var/lib/apt/lists/*
 
+RUN useradd -m -u 1000 builder
+USER builder
+
 WORKDIR /workdir
 
 COPY . .

Note: This may require adjusting file ownership and permissions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 1 - 16, Add a non-root user in the Dockerfile and
run the build steps as that user: create a user/group and set ownership of
WORKDIR (/workdir) and any copied files (after COPY . .) so subsequent RUN steps
(npm ci / pip install) and the final CMD execute as the non-root account; update
the Dockerfile to include the USER directive referencing that created user (and
adjust file permissions via chown on /workdir) before CMD so the container no
longer runs as root by default.
.dockerignore (1)

1-6: ⚡ Quick win

Consider excluding additional build artifacts and dependencies.

The current exclusions cover development metadata, but node_modules/ and build_output/ should also be excluded. Local node_modules/ will be reinstalled during npm ci (line 11 of Dockerfile), and including it in the build context unnecessarily bloats the context. Similarly, build_output/ is mounted at runtime (compose.yaml line 6) and doesn't need to be in the image.

📦 Proposed additions to reduce build context size
 .git
 .github
 .vscode
 .devcontainer
+node_modules/
+build_output/
 *.log
 *.tmp
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.dockerignore around lines 1 - 6, Update the .dockerignore to exclude local
build artifacts and dependencies by adding node_modules/ and build_output/ so
the Docker build context isn't bloated; specifically, modify the .dockerignore
(the file shown in the diff) to include entries for node_modules/ and
build_output/ alongside the existing patterns so local modules (reinstalled via
npm ci) and runtime-mounted build_output are not sent to the daemon.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@compose.yaml`:
- Line 7: The compose command references the shell variable PIO_ENV but it isn't
defined; either add an environment entry for PIO_ENV on the service (so
PlatformIO gets a concrete value) or document that users must supply PIO_ENV
when invoking compose (e.g., PIO_ENV=esp32dev docker compose up); update the
compose.yaml service that contains command ["bash", "-c", "npm run build && pio
run -e $PIO_ENV"] to include an env declaration for PIO_ENV or add a comment in
the file explaining the required external variable and an example invocation.

In `@Dockerfile`:
- Line 11: The RUN instruction currently falls back from "npm ci" to "npm
install" which defeats reproducible installs; remove the "|| npm install"
fallback so the Dockerfile runs only "npm ci" (i.e., replace the RUN line that
contains "npm ci || npm install" with a single "npm ci"), and ensure CI/build
tooling provides a correct package-lock.json and Node/npm in the base image so
the build fails loudly if "npm ci" cannot run.
- Line 1: The Dockerfile currently uses an unpinned base image "FROM
ubuntu:latest"; change this to a specific, immutable image tag or digest (for
example a release tag like ubuntu:22.04 or an image@sha256:<digest>) to ensure
reproducible builds—update the FROM line in the Dockerfile to use the chosen
pinned tag or digest and document the selected Ubuntu version.

---

Nitpick comments:
In @.dockerignore:
- Around line 1-6: Update the .dockerignore to exclude local build artifacts and
dependencies by adding node_modules/ and build_output/ so the Docker build
context isn't bloated; specifically, modify the .dockerignore (the file shown in
the diff) to include entries for node_modules/ and build_output/ alongside the
existing patterns so local modules (reinstalled via npm ci) and runtime-mounted
build_output are not sent to the daemon.

In `@Dockerfile`:
- Around line 1-16: Add a non-root user in the Dockerfile and run the build
steps as that user: create a user/group and set ownership of WORKDIR (/workdir)
and any copied files (after COPY . .) so subsequent RUN steps (npm ci / pip
install) and the final CMD execute as the non-root account; update the
Dockerfile to include the USER directive referencing that created user (and
adjust file permissions via chown on /workdir) before CMD so the container no
longer runs as root by default.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1a077bac-8a70-4d2c-bf98-a349b1db915c

📥 Commits

Reviewing files that changed from the base of the PR and between f68f91d and ee8ba6e.

📒 Files selected for processing (3)
  • .dockerignore
  • Dockerfile
  • compose.yaml

Comment thread compose.yaml
Comment thread Dockerfile
@@ -0,0 +1,16 @@
FROM ubuntu:latest
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin the base image to a specific version.

Using ubuntu:latest leads to non-reproducible builds because the underlying image can change at any time. For a containerized build environment, reproducibility is essential.

🔒 Proposed fix to pin Ubuntu version
-FROM ubuntu:latest
+FROM ubuntu:24.04
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FROM ubuntu:latest
FROM ubuntu:24.04
🧰 Tools
🪛 Trivy (0.69.3)

[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)


[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` at line 1, The Dockerfile currently uses an unpinned base image
"FROM ubuntu:latest"; change this to a specific, immutable image tag or digest
(for example a release tag like ubuntu:22.04 or an image@sha256:<digest>) to
ensure reproducible builds—update the FROM line in the Dockerfile to use the
chosen pinned tag or digest and document the selected Ubuntu version.

Comment thread Dockerfile Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
Dockerfile (1)

7-16: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a non-root runtime user to the image.

The container runs as root (no USER instruction), which weakens isolation and matches the DS-0002 finding. Add a non-root user before the CMD instruction:

Suggested fix
 ENV WLED_RELEASE=True
 
+RUN useradd --create-home --shell /bin/bash builder \
+    && chown -R builder:builder /workdir
+USER builder
+
 CMD ["bash"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 7 - 16, Add a non-root runtime user to the
Dockerfile to avoid running containers as root: create a user and group (e.g.,
myuser) and home dir after files are copied, chown /workdir (the WORKDIR used)
to that user, and add a USER instruction before CMD so subsequent runtime
processes (started by CMD ["bash"]) run as the non-root user; ensure any
build-time steps that need root still run earlier (e.g., RUN npm ci and RUN pip
install) and that file ownership for the workspace is transferred to the new
user.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@Dockerfile`:
- Around line 7-16: Add a non-root runtime user to the Dockerfile to avoid
running containers as root: create a user and group (e.g., myuser) and home dir
after files are copied, chown /workdir (the WORKDIR used) to that user, and add
a USER instruction before CMD so subsequent runtime processes (started by CMD
["bash"]) run as the non-root user; ensure any build-time steps that need root
still run earlier (e.g., RUN npm ci and RUN pip install) and that file ownership
for the workspace is transferred to the new user.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 769c5d94-c68a-4f38-bf59-9fd9bb6d9c06

📥 Commits

Reviewing files that changed from the base of the PR and between ee8ba6e and ec35731.

📒 Files selected for processing (3)
  • .dockerignore
  • Dockerfile
  • compose.yaml
✅ Files skipped from review due to trivial changes (1)
  • .dockerignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • compose.yaml

Comment thread Dockerfile Outdated
RUN npm ci
RUN pip install --break-system-packages --ignore-installed -r requirements.txt

ENV WLED_RELEASE=True
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is WLED_RELEASE needed for?

Its not checked by platformio or pio-scripts, neither referenced anywhere in our nightly CI jobs. SO maybe just remove it, to avoid future problems.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove it then. This was suggested in the intially drafted Dockerfile in the Discord conversation, I just copypasted it.

Comment thread .dockerignore
*.log
*.tmp
node_modules/
build_output/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this mean docker will ignore any firmware.bin files created by pio?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, they will not be copied inside the image (during the image build process).

But they will happen to be available (accidentally) during the actual firmware build phase, because the compose.yaml file bind-mounts the local build_output into the container (because we want to be able to retrieve those .bin files afterwards).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
Dockerfile (1)

7-7: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a non-root runtime user before the final CMD.

The Dockerfile currently lacks a USER directive, causing the container to run as root. This creates a problem with the bind-mounted build_output directory in compose.yaml: root-owned files will be written to the host filesystem.

Add the following before line 14 (CMD ["bash"]):

Proposed fix
 RUN npm ci
 RUN pip install --break-system-packages --ignore-installed -r requirements.txt
+
+RUN useradd --create-home --uid 1000 builder \
+    && chown -R builder:builder /workdir
+USER builder
 
 CMD ["bash"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` at line 7, Container currently runs as root because there is no
USER directive; create a non-root runtime user (e.g., "appuser"), ensure
ownership of the working directory set by WORKDIR /workdir is transferred to
that user (chown), and add USER appuser immediately before the final CMD
["bash"] so the process runs without root privileges; reference WORKDIR /workdir
and CMD ["bash"] to locate where to add the CREATE USER/CHOWN/USER steps.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Dockerfile`:
- Around line 3-5: The RUN layer currently installs python3-pip and may
encourage use of pip flags like --break-system-packages; instead create and use
an isolated virtualenv and install Python deps from there: in the Dockerfile
replace direct pip usage with steps to install python3-venv (if needed), run
python3 -m venv /opt/venv, upgrade pip via /opt/venv/bin/pip, install your
requirements via /opt/venv/bin/pip install -r requirements.txt (without
--break-system-packages or --ignore-installed), and update PATH (e.g. ENV
PATH="/opt/venv/bin:$PATH") so the rest of the image uses the venv-managed
Python; remove any global pip flags and avoid altering system packages.

---

Duplicate comments:
In `@Dockerfile`:
- Line 7: Container currently runs as root because there is no USER directive;
create a non-root runtime user (e.g., "appuser"), ensure ownership of the
working directory set by WORKDIR /workdir is transferred to that user (chown),
and add USER appuser immediately before the final CMD ["bash"] so the process
runs without root privileges; reference WORKDIR /workdir and CMD ["bash"] to
locate where to add the CREATE USER/CHOWN/USER steps.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0771b29b-3ca6-4d16-96df-b097328e44b3

📥 Commits

Reviewing files that changed from the base of the PR and between ec35731 and ab00feb.

📒 Files selected for processing (1)
  • Dockerfile

Comment thread Dockerfile
Comment on lines +3 to +5
RUN apt-get update \
&& apt-get install -y --no-install-recommends nodejs npm git ca-certificates python3-pip \
&& rm -rf /var/lib/apt/lists/*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Dockerfile uses venv-based Python deps and no system-break flags.
rg -n 'break-system-packages|ignore-installed|python3-venv|python3 -m venv|ENV PATH="/opt/venv/bin:\$PATH"' Dockerfile

Repository: wled/WLED

Length of output: 135


Avoid system-level pip override flags; install Python deps in a venv instead.

--break-system-packages and --ignore-installed reduce determinism and can hide dependency conflicts. Use an explicit virtual environment in the image.

Proposed fix
 RUN apt-get update \
-    && apt-get install -y --no-install-recommends nodejs npm git ca-certificates python3-pip \
+    && apt-get install -y --no-install-recommends nodejs npm git ca-certificates python3-pip python3-venv \
     && rm -rf /var/lib/apt/lists/*
@@
 RUN npm ci
-RUN pip install --break-system-packages --ignore-installed -r requirements.txt
+RUN python3 -m venv /opt/venv \
+    && /opt/venv/bin/pip install --no-cache-dir -r requirements.txt
+ENV PATH="/opt/venv/bin:$PATH"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 3 - 5, The RUN layer currently installs python3-pip
and may encourage use of pip flags like --break-system-packages; instead create
and use an isolated virtualenv and install Python deps from there: in the
Dockerfile replace direct pip usage with steps to install python3-venv (if
needed), run python3 -m venv /opt/venv, upgrade pip via /opt/venv/bin/pip,
install your requirements via /opt/venv/bin/pip install -r requirements.txt
(without --break-system-packages or --ignore-installed), and update PATH (e.g.
ENV PATH="/opt/venv/bin:$PATH") so the rest of the image uses the venv-managed
Python; remove any global pip flags and avoid altering system packages.

@willmmiles
Copy link
Copy Markdown
Member

willmmiles commented May 19, 2026

Why do we need a second container definition? There's already a Dockerfile in the .devcontainer folder, where it belongs.

@netmindz
Copy link
Copy Markdown
Member

This is redundant duplication, we already have a devcontainer

https://github.com/wled/WLED/tree/main/.devcontainer

@netmindz netmindz closed this May 20, 2026
@ondras
Copy link
Copy Markdown
Author

ondras commented May 20, 2026

Uhm... may I ask for some explanation here? The Dockerfile in question is basically empty, not suitable for building and/or running via the docker CLI.

Perhaps let me re-iterate the motivation for the PR: in my opinion, it is useful to have a way to build the project (the binary firmware file) without having to clutter the system with necessary prerequisities -- namely NodeJS, all npm dependencies, Python, PlatformIO and more. Docker is an ideal way to do so. So I would expect a straightforward way to use Docker in order to build the firmware; my Dockerfile (+compose.yaml) achieves that goal. The currently existing one does not.

Also, the .devcontainer directory -- is this a standardized place for Dockerfiles? As far as I can tell, this is something specific to one editor (VS Code).

@netmindz
Copy link
Copy Markdown
Member

The devcontainer is a vscode extension, but I should give everything needed to run all of your pio setup without the need for any extra tooling. Please try your compose with the existing Dockerfile to confirm. Maybe VSCode calls something you bootstrap the container?

@netmindz netmindz reopened this May 20, 2026
@ondras
Copy link
Copy Markdown
Author

ondras commented May 20, 2026

The devcontainer is a vscode extension, but I should give everything needed to run all of your pio setup without the need for any extra tooling. Please try your compose with the existing Dockerfile to confirm. Maybe VSCode calls something you bootstrap the container?

Trying to build & run the .devcontainer/Dockerfile:

$ docker build -f .devcontainer/Dockerfile -t wled --progress=plain .
#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 953B done
#1 DONE 0.0s

#2 [internal] load .dockerignore
#2 transferring context: 2B done
#2 DONE 0.0s

#3 [internal] load metadata for mcr.microsoft.com/devcontainers/python:0-3
#3 DONE 0.1s

#4 [1/1] FROM mcr.microsoft.com/devcontainers/python:0-3@sha256:452e43dd2381de67191c53f7692433baba156ee9138ccfaed5124ff26c63eaed
#4 CACHED

#5 exporting to image
#5 exporting layers done
#5 writing image sha256:49772e83e0c2edab5f45780ea53be7012cd55731ccabf5d6bd57ca7a6dc1dbff done
#5 naming to docker.io/library/wled done
#5 DONE 0.0s

$ docker run -it wled
Python 3.11.4 (main, Jun  7 2023, 18:32:58) [GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Apparently, this is just a plain image with a Python REPL interpreter. This corresponds to the fact that the Dockerfile is mostly empty. Also, it is somewhat suspicious that the image itself comes from Microsoft -- I guess that this particular file is strongly bound to some particlar VS Code extension (a piece of software that I do not use).

All in all, I do not think that this Dockerfile is sufficient/suitable for independent firmware building. Perhaps someone with a deeper knowledge of that extension can explain how is this supposed to work?

@netmindz
Copy link
Copy Markdown
Member

Yeah it looks like vscode must be doing some bootstrapping to install the dependencies automatically.

We should still try and standardise on a single Dockerfile as it's the same requirement - an environment to do your builds in

I think putting a docker-compose.yml into the root of the project will confuse more users than it would help

The approach of baking the source into the build image it also flawed, adding .gitignore doesn't resolve the issue - the source should be mounted into the builder image

@ondras
Copy link
Copy Markdown
Author

ondras commented May 20, 2026

I think putting a docker-compose.yml into the root of the project will confuse more users than it would help

I have no problem with moving that file to other location.

The approach of baking the source into the build image it also flawed, adding .gitignore doesn't resolve the issue - the source should be mounted into the builder image

Okay then -- what do you propose should be in the image? We can only copy the definition of dependencies (e.g. package.json and requirements.txt) and install those during the build phase. The whole source tree will be then mounted (along with the output build dir) at runtime via bind-mounts specified in the compose.yaml.

I am not sure if this is a suitable approach, though: bind-mounting the source makes the image "always up to date", but changing a dependency breaks the build completely (as the container will attempt to build the mounted source with out-of-the-date dependencies).

@ondras
Copy link
Copy Markdown
Author

ondras commented May 20, 2026

Giving this some more thought: apparently for the purpose of "building a firmware binary in an isolated environment", the difference between a build phase and a runtime phase is very small. To summarize our options, we can:

  1. Build the firmware during image build. This means only specifying stuff in the Dockerfile and this approach has been actually suggested by the very first draft posted in a Discord discussion by Sören.

  2. Do nothing in the image build (just use a suitable baseimage) and perform the whole shebang at runtime, via a script mounted (and executed) inside of the image. This approach basically ignores the existence of a docker image, as we always start on a clean slate.

  3. Perform a "setup phase" (build deps) when building and continue with the compilation at runtime.

From the point of view of a "one-shot build", I do not see any differences between these approaches. But if we consider the lifecycle of a container (data created at runtime are stored in a temporary writable FS layer that overlays the image), IMHO it is useful to think of the container as of something easily disposable. And for repeated builds (e. g. for multiple targets), we probably do not want to install the deps every time we build. So from my point of view, the approach 3 makes the most sense (as it allows persistent caching of build dependencies -- something that does not change often when compared to the source code itself).

@willmmiles
Copy link
Copy Markdown
Member

Also, the .devcontainer directory -- is this a standardized place for Dockerfiles? As far as I can tell, this is something specific to one editor (VS Code).

Yes, it's an attempt to create a standard place for containerized software development with a composable featureset of tools. The tooling is homed here: https://containers.dev/ -- while it was created to smooth the experience for VSCode, using VSCode is not required for using the devcontainer standard. The devcontainer command line tool can be used to assemble a container from the specification.

Yeah it looks like vscode must be doing some bootstrapping to install the dependencies automatically.

That's also correct - devcontainer.json instructs VSCode to install the platformio.platformio-ide extension, which in turn bootstraps PlatformIO. If we wanted to have PlatformIO pre-installed we could switch to a standard image and adopt the PlatformIO feature:

"image": "mcr.microsoft.com/devcontainers/python:3",
"features": {
    "ghcr.io/ar90n/devcontainer-features/platformio:1": {}
}

(Note that the PlatformIO VSCode extension will pretty much never use a pre-installed version -- it always wants to install its own captive version. The option to enable using an external PlatformIO install seems generally broken.)

Same could be done for our npm install I think.


To sum up: I think it's definitely true that our devcontainer.json could use some attention to produce something that works out-of-the-box for development with other tooling. I would prefer to stay within the devcontainer framework for managing container specifications, if in no small part because it integrates directly with Github Codespaces, allowing people to make custom builds without having to deploy a local container environment.

@ondras
Copy link
Copy Markdown
Author

ondras commented May 21, 2026

To sum up: I think it's definitely true that our devcontainer.json could use some attention to produce something that works out-of-the-box for development with other tooling. I would prefer to stay within the devcontainer framework for managing container specifications, if in no small part because it integrates directly with Github Codespaces, allowing people to make custom builds without having to deploy a local container environment.

I think that we are in full agreement: the ability to build binary firmware in an isolated environment is the goal; individual tools that enable it are not that important.

I am okay with using the devcontainer feature as long as it is available in my non-IDE/non-GUI/non-VSCode setup. Is this something I can use right now to build the firmware?

@ondras
Copy link
Copy Markdown
Author

ondras commented May 21, 2026

I tried reading more about the whole devcontainer concept and honestly I am really puzzled about it. The official website does not seem to answer the most basic questions -- why is this abstraction necessary, why they decided not to use regular Dockerfiles, why they decided not to use (and/or extend) docker compose. I've been working with Docker-based products for ~10 years and it seems that a container+portforward+bindmounts (orchestrated via compose) was always sufficient for all development needs.

@willmmiles
Copy link
Copy Markdown
Member

I tried reading more about the whole devcontainer concept and honestly I am really puzzled about it. The official website does not seem to answer the most basic questions -- why is this abstraction necessary, why they decided not to use regular Dockerfiles, why they decided not to use (and/or extend) docker compose. I've been working with Docker-based products for ~10 years and it seems that a container+portforward+bindmounts (orchestrated via compose) was always sufficient for all development needs.

The key limitation with pure Docker tooling is composability -- you can't trivially mix+match layers to construct a final environment. The devcontainer tooling brings a registry of "features" -- essentially, publicly maintained script snippets to install a certain tool -- that can be stacked arbitrarily to compose the final image. It makes it easy to have this project include (nodejs + platformio), that project (nodejs + aws), the next one (aws + conda), and so forth. And best of all, I don't have to maintain copies of those snippets in every one of my projects; they're implemented as references to public repositories.

Essentially the devcontainer spec is a way of constructing a unique project-specific dockerfile from publicly available fragments that do not have a rigid layer ordering.

I sketched a more-complete devcontainer at https://github.com/willmmiles/WLED/tree/devcontainer-completeness -- you're welcome to give it a try.

@ondras
Copy link
Copy Markdown
Author

ondras commented May 22, 2026

I sketched a more-complete devcontainer at https://github.com/willmmiles/WLED/tree/devcontainer-completeness -- you're welcome to give it a try.

Well, I tried -- it did something:

$ devcontainer up
[3 ms] @devcontainers/cli 0.87.0. Node.js v22.22.2. linux 7.0.0-14-generic x64.
{"outcome":"success","containerId":"04f84fabbc2dae6ccb61da10df89ee3e85fe918d67ab2723eba153b5803ba7b1","remoteUser":"vscode","remoteWorkspaceFolder":"/workspaces/WLED"}

What exactly is the next step? I tried execing into the running container:

$ devcontainer exec bash
vscode ➜ /workspaces/WLED (main) $ pio
bash: pio: command not found

@willmmiles
Copy link
Copy Markdown
Member

I sketched a more-complete devcontainer at https://github.com/willmmiles/WLED/tree/devcontainer-completeness -- you're welcome to give it a try.

Well, I tried -- it did something:
What exactly is the next step? I tried execing into the running container:

$ devcontainer exec bash
vscode ➜ /workspaces/WLED (main) $ pio
bash: pio: command not found

Hm - that's pretty much exactly what I did?

wmiles@Alcyone:~/Projects/WLED4$ git checkout devcontainer-completeness
branch 'devcontainer-completeness' set up to track 'origin/devcontainer-completeness'.
Switched to a new branch 'devcontainer-completeness'
wmiles@Alcyone:~/Projects/WLED4$ devcontainer up
[1 ms] @devcontainers/cli 0.87.0. Node.js v22.18.0. linux 6.6.87.2-microsoft-standard-WSL2 x64.
[865 ms] Resolving Feature dependencies for 'ghcr.io/ar90n/devcontainer-features/platformio:1'...
[1513 ms] Resolving Feature dependencies for 'ghcr.io/devcontainers/features/node:2'...
<output snipped>
wmiles@Alcyone:~/Projects/WLED4$ devcontainer exec bash
vscode ➜ /workspaces/WLED4 (devcontainer-completeness) $ pio run -e nodemcuv2
Processing nodemcuv2 (board: nodemcuv2; platform: espressif8266@4.2.1; framework: arduino)
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
<output snipped>

The CLI tool doesn't seem clever enough to notice that the devcontainer.json file has been updated when you switch branches. You might need to run devcontainer up --remove-existing-container to get it to rebuild the container with the new branch, otherwise it will continue with the container built for the previous branch.

@ondras
Copy link
Copy Markdown
Author

ondras commented May 22, 2026

Sorry, my bad! I forgot that your fix lives in a branch and tried the whole stuff in main. It works now -- the firmware is compiled.

The only small non-fatal problem I encountered when running pio run -e esp32dev is this red-colored log fragment:

Installing collected packages: zopfli, wheel, pyyaml, tasmota-metrics
ERROR: Could not install packages due to an OSError: [Errno 13] Permission denied: '/usr/local/py-utils/venvs/platformio/lib/python3.14/site-packages/zopfli'
Check the permissions.


*** Error 1

Nevertheless, the goal of having a working isolated build environment has been achieved 👍 and I would welcome merging the updated devcontainer definition into the mainline WLED. Now that my PR has been obsoleted, I can at least provide a new PR with updated documentation for those devs (me included) who would benefit from the new build env. Is this something desirable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants