Since we are working with containers and VS Code is pretty popular this post aims to show you how to integrate a Zephyr containers into VS Code, the procedure basically is already written in the following post https://embedded-house.ghost.io/part-7-docker-for-vscode-lovers/ , but this time will be specific for the modularmx/zephyros container.
Make a new file called compose.yml on the root folder of your project that we previously made in https://embedded-house.ghost.io/working-with-manifests/
$ code compose.yml
And write the following content
services:
zephyr: # container name been used by the image to connect to make
image: modularmx/zephyros:latest
working_dir: /home/user/workspace
volumes: # volume mapping
- type: bind
source: ./
target: /home/user/workspace
command: # keep the container running
sleep infinity
Also create the corresponding devcontianer file in the same directory
{
// Name of the dev container
"name": "devtools",
// compose file to run containers
"dockerComposeFile": "compose.yml",
// indicate the container to run from the compose file
"service": "zephyr",
// indicate the working directory inside the container
"workspaceFolder": "/home/user/workspace",
// set bash as the default shell
"postStartCommand": "/bin/bash",
// set some usefull vs code configuration only exsiting in our contianer
"customizations": {
"vscode": {
// Add some extensions
"extensions": [
"ms-vscode.cpptools",
"trond-snekvik.gnu-mapfiles",
"twxs.cmake"
],
// Set a fancy themes
"settings": {
"workbench.colorTheme": "One Dark Pro Night Flat"
}
}
}
}
Let me refresh your mind about our project directory structure, and yes I’m using a manifest, so get used to it.
.
├── app
├── build
├── compose.yml
├── deps
├── .devcontainer.json
└── .west
In VS Code press Ctrl + p
and select Dev Contianers: Reopen in Container. Now you are basically running the container in VS Code, how cool is that, and from here you can build your project, and so on. Again I’m assuming you have the docker plugin installed in your VSCode

Debugging with VS Code
What about debugging?, I know we can debug using the cortex debug plugin inside VS Code AN003: Debugging with VS Code , Well, lets get to it, add JLink just like we did in the previous post Using Containers for good. Modify the compose.yml to change image field with any name you decided to gave, in my case is “west_jlink” and grant access to USB
services:
zephyr: # container name
image: west_jlink # image with zephyr and j-Link
working_dir: /home/user/workspace
volumes: # mount directory
- type: bind
source: ./
target: /home/user/workspace
devices: # device mapping to usb ports
- /dev/bus/usb:/dev/bus/usb
command: # keep the container running
sleep infinity
Also add a the cortex-debug plugin to our devcontainer file.
// Add some extensions
"extensions": [
"ms-vscode.cpptools",
"trond-snekvik.gnu-mapfiles",
"twxs.cmake",
"marus25.cortex-debug"
],
Write a launch.json file as usually inside a .vscode folder, it is important to indicate the SDV file and the most important one the gdbPath, this is the path where the arm gdb can be found inside our container. In case using openocd adjust accordingly.
{
"version": "1.12.1",
"configurations": [
{
"type": "cortex-debug",
"request": "launch",
"name": "Debug (J-Link)",
"servertype": "jlink",
"interface": "swd",
"cwd": "${workspaceRoot}",
"runToEntryPoint": "main",
"executable": "${workspaceRoot}/build/zephyr/zephyr.elf",
"device": "nrf54l15_m33",
"svdFile": "${workspaceRoot}/deps/modules/hal/nordic/nrfx/mdk/nrf54l15_application.svd",
"armToolchainPath": "/opt/zephyr-sdk/arm-zephyr-eabi/bin",
"toolchainPrefix": "arm-zephyr-eabi"
}
]
}
Launch a debug session as you normally would do, and that’s it!, now you can build, flash and debug with your lovely favorite editor.

Using two containers
Now lets imagine you already have a separate container with your J-Link/OpenOCD server and don’t want to modify our Zephyr container, we can orchestrate both with compose Part 4: Time to Docker Compose and use the same devcontianer of course, just modify the compose file in the following way
services:
server: # container name
image: myjlink
ports: # port mapping
- 2331:2331
devices: # device mapping to usb ports
- /dev/bus/usb:/dev/bus/usb
networks:
static_net: # use network static_net
ipv4_address: 172.25.0.2 # assing ip address to container
command: JLinkGDBServer -nolocalhostonly -device nrf54l15_m33 -if swd -speed 4000
zephyr: # container name
image: modularmx/zephyros:latest
working_dir: /home/user/workspace
volumes: # volume mapping
- type: bind
source: ./
target: /home/user/workspace
depends_on:
- server
networks:
static_net: # use network static_net
ipv4_address: 172.25.0.3 # assing ip address to container
command: # keep the container running
sleep infinity
networks:
static_net: # network name to be used by containers
driver: bridge # network driver
ipam:
config: # network configuration
- subnet: 172.25.0.0/16 # network subnet
gateway: 172.25.0.1 # network gateway
We need to modify our .vscode/launch.json changing the server type to “external“ and indicating the IP assigned to our debug server container. With this you can launch you debug session as before, to see the same results. This is my favorite of both ways since I have the flexibility to change my tools and avoid modifications in my containers.
{
"version": "1.12.1",
"configurations": [
{
"type": "cortex-debug",
"request": "launch",
"name": "Debug (J-Link)",
"servertype": "external",
"gdbTarget": "172.25.0.2:2331",
"interface": "swd",
"cwd": "${workspaceRoot}",
"runToEntryPoint": "main",
"executable": "${workspaceRoot}/build/zephyr/zephyr.elf",
"svdFile": "${workspaceRoot}/deps/modules/hal/nordic/nrfx/mdk/nrf54l15_application.svd",
"armToolchainPath": "/opt/zephyr-sdk/arm-zephyr-eabi/bin",
"toolchainPrefix": "arm-zephyr-eabi"
}
]
}
Serial monitor
By the way if you want to monitor the serial port in you container you can use the plugin serial monitor from Microsoft, just add it in the devcontainer file, but also you need add one extra package for your container using postCreateCommand
option and set permission to our “user” in dialout group, otherwise the plugin could not connected to the serial port
// execute commands after the container is created
"postCreateCommand": "sudo apt-get update && sudo apt-get install -y udev && sudo usermod -aG dialout user",
// set bash as the default shell
"postStartCommand": "/bin/bash",
// set some usefull vs code configuration only exsiting in our contianer
"customizations": {
"vscode": {
// Add some extensions
"extensions": [
"ms-vscode.cpptools",
"trond-snekvik.gnu-mapfiles",
"twxs.cmake",
"plorefice.devicetree",
"marus25.cortex-debug",
"ms-vscode.vscode-serial-monitor"
],
The serial port in the case of our Nordic board will appear as ttyACM1

Using Ozone
But you know I really like Ozone, can we use ozone instead of cortex-debut, still I want VS Code to run the zephyr container
services:
ozone: # container name
image: myozone
devices: # device mapping to usb ports
- /dev/bus/usb:/dev/bus/usb
working_dir: /home/user/workspace
volumes: # volume mapping
- type: bind
source: ./
target: /home/user/workspace
environment:
- DISPLAY=${DISPLAY}
network_mode: host
depends_on: # run until zephyr is up and running first
- zephyr
command: # keep the container running
ozone
zephyr: # container name
image: modularmx/zephyros:latest
working_dir: /home/user/workspace
volumes: # volume mapping
- type: bind
source: ./
target: /home/user/workspace
command: # keep the container running
sleep infinity
Once you run the devcontainer ozone will open, from here and after you build your project you have to proceed as in previous post or as in shown in this video Ozone – The J-Link Debugger | Introduction . If you close ozone window you will have to rebuild to reopen your devcontianer again…

If you want to launch in auto you debug session, meaning with the device and the source file selected you can append the following to line 17 in the previous compose file, but you need to previously build your project
command: # keep the container running
ozone -device nrf54l15_m33 -select USB -if SWD -speed 4000 -programfile /home/user/workspace/build/zephyr/zephyr.elf
If you don’t want to luach ozone inmedialty you can replace the prevosus commnad
command: # keep the container running
sleep infinity
Then, in another terminal outside VS Code connect to the ozone running container
$ docker exec -it zephyr-ozone-1 ozone -device nrf54l15_m33 -select USB -if SWD -speed 4000 -programfile /home/user/workspace/build/zephyr/zephyr.elf
Hope this can help you to work with containers in a more automated way, and choose wherever option you think is more appropriate for you, see you in the road