Developing Firmware With A Docker Container

Moving your build environment into a docker container has a lot of advantages. In this post we'll demonstrate what developing inside a docker container actually looks like.

First we'll pull a Docker Image that has the software we need from a hosted site like Docker Hub.

For this example we're going to want {% c-line %}make{% c-line-end %} for building our project and {% c-line %}mergehex{% c-line-end %} from Nordic's {% c-line %}nrfutil{% c-line-end %} package for merging two hex images together into one hex.

The image lagerdata/devenv-cortexm-nrf52 has what we need so we'll use that.

{% c-block language="bash" %}
docker pull lagerdata/devenv-cortexm-nrf52
{% c-block-end %}

Once the image finishes downloading you can verify it's available by running the following:

{% c-block language="console" %}
% docker image ls
REPOSITORY                         TAG                   IMAGE ID       CREATED        SIZE
lagerdata/devenv-cortexm-nrf52     latest                5558006acaab   9 days ago     2.97GB
{% c-block-end %}

Next we'll clone our project, in this case the nRF5 SDK from NordicSemi.

{% c-block language="console" %}
% git clone
{% c-block-end %}

Now let's mount the SDK into our docker container and start the container.

{% c-block language="console" %}
docker run -it --rm -w /app -v $PWD:/app lagerdata/devenv-cortexm-nrf52 /bin/bash
{% c-block-end %}

Before continuing let's review what's going on in the above command.

{% c-line %}docker run{% c-line-end %} - starts a container using the given image

{% c-line %}-it{% c-line-end %} - run the container in interactive (i) tty (t) mode

{% c-line %}--rm{% c-line-end %} - remove/kill the container when exiting

{% c-line %}-w{% c-line-end %} - enter the container at the given working directory, in this case /app

{% c-line %}-v{% c-line-end %} - mount the local host folder into the container at the given location, i.e.{% c-line %}$PWD{% c-line-end %} → {% c-line %}/app{% c-line-end %}

{% c-line %}lagerdata/devenv-cortexm-nrf52{% c-line-end %} - create container instance from this image

{% c-line %}/bin/bash{% c-line-end %} - run bash

Once we're inside the container (via bash), we can navigate to any of the example projects and build it like we normally would:

{% c-block language="console" %}
% cd examples/ble_peripheral/ble_app_blinky/pca10056/s140/armgcc
% make
mkdir _build
cd _build && mkdir nrf52840_xxaa
Assembling file: gcc_startup_nrf52840.S
Compiling file: nrf_log_backend_rtt.c
Compiling file: nrf_log_backend_serial.c
Compiling file: nrf_log_backend_uart.c
Compiling file: nrf_log_default_backends.c
Compiling file: nrf_log_frontend.c
Compiling file: nrf_log_str_formatter.c
Compiling file: app_button.c
Compiling file: app_error.c
Compiling file: app_error_handler_gcc.c
Compiling file: app_error_weak.c
Compiling file: app_scheduler.c
Compiling file: app_timer2.c
Compiling file: app_util_platform.c
Compiling file: drv_rtc.c
Compiling file: hardfault_implementation.c
Compiling file: nrf_assert.c
Compiling file: nrf_atfifo.c
Compiling file: nrf_atflags.c
Compiling file: nrf_atomic.c
Compiling file: nrf_balloc.c
Compiling file: nrf_fprintf.c
Compiling file: nrf_fprintf_format.c
Compiling file: nrf_memobj.c
Compiling file: nrf_pwr_mgmt.c
Compiling file: nrf_ringbuf.c
Compiling file: nrf_section_iter.c
Compiling file: nrf_sortlist.c
Compiling file: nrf_strerror.c
Compiling file: system_nrf52840.c
Compiling file: boards.c
Compiling file: nrf_drv_clock.c
Compiling file: nrf_drv_uart.c
Compiling file: nrfx_atomic.c
Compiling file: nrfx_clock.c
Compiling file: nrfx_gpiote.c
Compiling file: nrfx_prs.c
Compiling file: nrfx_uart.c
Compiling file: nrfx_uarte.c
Compiling file: main.c
Compiling file: SEGGER_RTT.c
Compiling file: SEGGER_RTT_Syscalls_GCC.c
Compiling file: SEGGER_RTT_printf.c
Compiling file: ble_advdata.c
Compiling file: ble_conn_params.c
Compiling file: ble_conn_state.c
Compiling file: ble_srv_common.c
Compiling file: nrf_ble_gatt.c
Compiling file: nrf_ble_qwr.c
Compiling file: utf.c
Compiling file: ble_lbs.c
Compiling file: nrf_sdh.c
Compiling file: nrf_sdh_ble.c
Compiling file: nrf_sdh_soc.c
Linking target: _build/nrf52840_xxaa.out
  text   data    bss    dec    hex filename
 28960    176   2520  31656   7ba8 _build/nrf52840_xxaa.out
Preparing: _build/nrf52840_xxaa.hex
Preparing: _build/nrf52840_xxaa.bin
DONE nrf52840_xxaa
{% c-block-end %}

Finally let's go ahead and merge the nordic's bluetooth stack image with the application image we just built:

{% c-block language="console" %}
% mergehex -m examples/ble_peripheral/ble_app_blinky/pca10056/s140/armgcc/_build/nrf52840_xxaa.hex \
components/softdevice/s140/hex/s140_nrf52_7.2.0_softdevice.hex \
-o sd_app.hex
Parsing input hex files.
Merging files.
Storing merged file.
components  documentation  external        integration  module
sconfig      examples       external_tools  license.txt  sd_app.hex
{% c-block-end %}

And the nice thing is that when we exit the container, all the assets that were created remain on the host's file system.

One issue you may have considered is that IDE options inside a docker container are limited. For example if we're trying to keep the image size as small as possible for portability reasons we may not even have {% c-line %}nano{% c-line-end %} installed. An easy work around is to create aliases in your host machine's bash file to execute common commands you would normally run in the docker container. For example to build your project from your host machine you could add this to your {% c-line %}.zprofile{% c-line-end %} file:

{% c-block language="console" %}
alias build_app='docker run -it --rm -w /app -v path/to/project:/app lagerdata/devenv-cortexm-nrf52  /bin/bash -c '"'"'cd examples/ble_peripheral/ble_app_blinky/pca10056/s140/armgcc;make'"'"
{% c-block-end %}

{% c-block language="console" %}
alias clean_app='docker run -it --rm -w /app -v path/to/project:/app lagerdata/devenv-cortexm-nrf52  /bin/bash -c '"'"'cd examples/ble_peripheral/ble_app_blinky/pca10056/s140/armgcc;make clean'"'"
{% c-block-end %}

{% c-block language="console" %}
alias clean_app='docker run -it --rm -w /app -v path/to/project:/app lagerdata/devenv-cortexm-nrf52  /bin/bash -c '"'"'cd examples/ble_peripheral/ble_app_blinky/pca10056/s140/armgcc;make clean'"'"
{% c-block-end %}

This allows you to still take full advantage of using Docker Containers as your build environment without needing to leave your host environment.

Shameless Lager Plug!

Of course if you want to avoid creating aliases, and use a method for creating Docker commands that are portable across Linux, MacOS, and Windows, you could use Lager.

First install Lager via pip ({% c-line %}pip install lager-cli{% c-line-end %}). Then run the following:

{% c-block language="console" %}
% lager devenv create
Docker image [lagerdata/devenv-cortexm]: lagerdata/devenv-cortexm-nrf52
Source code mount directory in docker container [/app]: /app
{% c-block-end %}

This will create a {% c-line %}.lager{% c-line-end %} file in your project. Now you can do the following to save and run commands:

{% c-block language="console" %}
#create and run shortcut called "build" that runs make inside docker container
% lager exec --command "cd examples/ble_peripheral/ble_app_blinky/pca10056/s140/armgcc; make" --save-as build
#create and run shortcut called "clean" that cleans the project using make
% lager exec --command "cd examples/ble_peripheral/ble_app_blinky/pca10056/s140/armgcc; make clean" --save-as clean
{% c-block-end %}

After that you just run the following to build and clean your project:

{% c-block language="console" %}
% lager exec build
% lager exec clean
{% c-block-end %}

And because the commands get stored in the {% c-line %}.lager{% c-line-end %} file, as long as you add that file to your repository, anyone who clones your project can simply run {% c-line %}lager exec build{% c-line-end %} to start building, no need to install anything!

Ok, so to recap why this is so powerful, imagine a second engineer is joining your project, and they are ready to start ASAP. There are only 2 steps required for them to get going:

  1. {% c-line %}git clone ...{% c-line-end %}
  2. {% c-line %}lager exec build{% c-line-end %}

And don't forget this is OS and system environment independent. As long as they can run a Docker Container, it will JustWork™.

Want to stay up to date on the future of firmware? Join our mailing list.