How I Created a Containerized Zephyr Build Environment Using Linux, Docker, and VS Code — and How You Can Too!
This technical overview details a Zephyr application development environment that is reproducible, portable, fully isolated, uses open source tools, and is simple to use. The isolation provided by Docker, paired with the general-purpose Swiss army knife functionality of VS Code, is a versatile foundation. Combine that with the capability and power of the Zephyr RTOS and its large number of target hardware platforms, and the result is a setup that is hard to beat. When properly configured to work together, these independent tools and technologies synergize to produce a powerful embedded firmware development environment. Establishing this workflow necessitated overcoming significant integration hurdles that required an exhausting process of trial and error.
This talk will cover the presenter's experience creating the environment for developing a commercial Zephyr-based product, point out the issues encountered, the solutions discovered, and share the information and resources needed for others to recreate it for their own use. The final result is a set of configuration files that enables VS Code to automatically construct the development environment and requires only minor manual configuration. By removing weeks of laborious setup, this solution helps developers start building almost immediately while reaping the advantages outlined above.
What this presentation is about and why it matters
How do you keep a Zephyr build environment stable when the host machine, OS updates, tool versions, and team laptops all keep changing? David Smith approaches that problem as a practical containerization case study, grounded in an embedded product cycle that had to move quickly. He walks through the architecture he assembled with Linux, Docker, VS Code, and a virtual machine, then shows how it is set up, launched, and used for building, flashing, and debugging Zephyr applications. This is most useful if you are wrestling with reproducible embedded development, multi-developer setups, or the headache of making a build system survive over time.
Who will benefit the most from this presentation
- Embedded firmware engineers who need a repeatable Zephyr setup across multiple machines
- Team leads or tech leads responsible for onboarding developers onto an embedded project
- Developers who are fighting host-specific build failures, tool drift, or environment rot
- Engineers using VS Code workflows and wanting containerized build and debug integration
- Teams targeting long-lived products where toolchain support and machine turnover are concerns
What you need to know
This talk is easiest to follow if you already know the basics of embedded development and have at least some familiarity with Zephyr and VS Code.
- Basic understanding of embedded build and flash workflows
- Familiarity with Zephyr concepts such as
west, build, and flash steps - Some exposure to Docker or container-based development
- Comfort with VS Code extensions and configuration files like JSON
Glossary (terms used in this talk)
- Development environment entropy: The accumulation of small, unmanaged differences in tools, dependencies, operating systems, and machine setup that makes a build environment drift over time. It is often experienced as inconsistent builds, fragile setup, or the familiar "works on my machine" problem.
- Dev Containers: A workflow for defining a development environment in configuration so it can be recreated inside a container. It helps make the editor, toolchain, and project workspace more consistent across machines.
- RTT (Real-Time Transfer): A debug and trace channel that can stream text or data between target hardware and a host tool while the program is running. It is often used for console-style output without relying on a conventional serial port.
- Device Tree: A data-description format used to describe the hardware layout of a board so that application code is decoupled from pin numbers and peripheral addresses.
- Kconfig: A menu-driven build configuration system used to enable or disable features at compile time so only required modules are included.
Toolbox (mentioned in this talk)
- GitHub: A web-based platform for hosting Git repositories and collaborating on software development.
- Zephyr RTOS: A scalable real-time operating system for connected and resource-constrained devices. It often owns much of the system startup flow, including hardware initialization and thread setup.
- Visual Studio Code: A lightweight source code editor with broad language support and extension-based integration. It is often used for embedded development and can pair well with containerized workflows.
- J-Link: A family of debug probes used for programming and debugging embedded targets over standard interfaces such as JTAG and SWD. It is widely used for interactive bring-up and firmware inspection.
- GDB: The GNU Debugger, used to inspect program state, stack traces, and variables after a crash or during a debug session. It is a standard tool for understanding why a fuzzed input triggered a failure.
- Linux: A family of open-source operating systems widely used as a host environment for embedded development, servers, and development tools. It provides a broad base of runtime services, libraries, and utilities for building and running software.
Final thoughts
Practical and experience-driven, this talk gives you a design vocabulary for turning a fragile embedded setup into something more repeatable and easier to hand off. The value is less about one specific tool and more about seeing how the pieces fit together, what belongs in the host, what belongs in the container, and where the friction tends to show up. Embedded developers, especially those on Zephyr projects or multi-person teams, will get the most out of it. It is a grounded look at making the day-to-day workflow feel less brittle.
This overview is AI-generated from the session transcript. Spot an issue? Let us know.
@davek I'm glad that you enjoyed the presentation! Thank you very much for your kind comments.
I’ve created a similar solution, but I didn’t not think of running it onto a VM, only running the container.
One of the issues I ran into with putting Zephyr and associated tools into the container is the size of the docker image. What’s the size of your image? Do you have a strategy on keeping the size of image below 8GB (I arbitrarily picked this number)?
@M3L11 The containerized build is where most of the magic happens, but adding in the VM layer solved another class of problems I was running into where my company's IT policy either required: 1) periodically migrating to a new OS version on our PCs (win 10 to 11 for example) or 2) moving to a completely new PC. Before using the VM layer, this required installing the supporting tools (Docker, VS Code, JLink tools, the dev environment project, etc) from scratch each time. With the VM layer, I simply have to install VirtualBox on the new host, plug in my external drive containing my VMs, and I'm almost immediately up and running again.
Your question about the resulting Docker image size when installing the Zephyr SDK and Zephyr code is an insightful one. My Docker images are very LARGE. The one for the demo used in the presentation is 26.2GB. It is built starting with the official Zephyr SDK Docker image (most of the tools), which is 20.2GB itself before adding the Zephyr code. Fortunately, non-volatile storage is cheap.
Thanks for watching the presentation and for the question.
David, I wonder how do you deal with updates to the virtual OS and updates within the containers, especially the build system, as this leads sometimes to different code as the manufacturer fixes bugs. How then do you redistribute the VM to your team and make sure, they adapt to it?
Thanks for your great presentation!
/Thomas
Hi Thomas. Thanks for the comments and questions!
Let's start with the containers - the Dockerfile uses version pinning to build exactly the same version each time it builds an image. This ensures that none of the tooling changes on the developer UNLESS/UNTIL the developer specifically changes the Dockerfile to use a newer version. That way, tooling and version changes are intentional acts -- the developer must take explicit action to upgrade to a newer version by modifying the Dockerfile to pin it to that newer version. This type of "upgrade" can be scheduled into the development process when convenient for the team, instead of being forced on the developer by the tooling itself.
For the guest OS VM, a good suggestion would be to use an LTS (long term support) version of the Linux distro of choice so you have options over a longer term. Since it is a VM isolated from the host OS, I typically tend to avoid updates to it unless there is something in the update that I'd absolutely need.
Our team is small, so the method we use to update everyone when we make a change to the development environment is pretty simple, but may scale to medium sized teams as well. For an update, once I have modified the Dockerfile, tested, and verified it, I push the updated config files to Github and have the rest of the team pull down the new build environment config files. These can be placed in a new top-level workspace so that it can co-exist with the existing version if desired. The team then builds the new docker image as discussed in the talk, and subsequently pulls the project files (the application being developed) from Github into the desired "app" folder.
As an alternative to the J-Link, if I were to use OpenOCD then how should the docker config be updated to reflect the different debug adapter?
@xjordanx While building this setup, I did see comments from others (in blog posts, forums, etc) about the potential to use OpenOCD. The programming adapter (either JLink or OpenOCD) affects two primary functions: the ability to "flash" the compiled image to the target and the ability to do source-level debugging.
To use the OpenOCD programmer, you'd need to research the correct options and then update the following files in each project folder:
- ./.vscode/tasks.json - modify the section that describes flashing the image to use the OpenOCD flash runner.
- ./.vscode/launch.json - modify the cortex debug extension settings to use OpenOCD.
Also, you'd need to replace the JLink version of the GDB Server with GDB Server supporting OpenOCD.
I'd expect it will take some trial and error and experimentation to arrive at the working configuration for OpenOCD just like it did for me getting it working with JLink.
Link to the Github repo of project files at 33min:35sec.








I thoroughly enjoyed your presentation. I especially liked the slides that show where the different components reside and which parts they communicate with. Well done, my friend.