Improve this doc

Define a container

Balena uses Docker containers to manage applications. You can use one or more containers to package your services with whichever environments and tools they need to run.

To ensure a service has everything it needs, you'll want to create a list of instructions for building a container image. Whether the build process is done on your device, on your workstation, or on the balena builders, the end result is a read-only image that ends up on your device. This image is used by the container engine (balena or Docker, depending on the balenaOS version) to kick off a running container.

Dockerfiles

The instructions for building a container image are written in a Dockerfile - this is similar to a Makefile in that it contains a recipe or set of instructions to build our container.

The syntax of Dockerfiles is fairly simple - at core there are 2 valid entries in a Dockerfile - comments, prepended with # as in script files, and instructions of the format INSTRUCTION arguments.

Typically you will only need to use 4 instructions - FROM, RUN and ADD or COPY:-

  • FROM has to be the first instruction in any valid Dockerfile and defines the base image to use as the basis for the container you're building.

  • RUN simply executes commands in the container - this can be of the format of a single line to execute, e.g. RUN apt-get -y update which will be run via /bin/sh -c, or [ "executable", "param1", "param2", ... ] which is executed directly.

  • ADD copies files from the current directory into the container, e.g. ADD <src> <dest>. Note that if <dest> doesn't exist, it will be created for you, e.g. if you specify a folder. If the <src> is a local tar archive it will unpack it for you. It also allows the <src> to be a url but will not unpack remote urls.

  • COPY is very similar to ADD, but without the compression and url functionality. According to the Dockerfile best practices, you should always use COPY unless the auto-extraction capability of ADD is needed.

  • CMD this command provides defaults for an executing container. This command will be run when the container starts up on your device, whereas RUN commands will be executed on our build servers. In a balena application, this is typically used to execute a start script or entrypoint for the users application. CMD should always be the last command in your Dockerfile. The only processes that will run inside the container are the CMD command and all processes that it spawns.

For details on other instructions, consult the official Dockerfile documentation.

Using Dockerfiles with balena

To deploy a single-container application to balena, simply place a Dockerfile at the root of your repository. A docker-compose.yml file will be automatically generated, ensuring your container has host networking, is privileged, and has lib/modules, /lib/firmware, and /run/dbus bind mounted into the container. The default docker-compose.yml will look something like this:

version: '2.1'
networks: {}
volumes:
  resin-data: {}
services:
  main:
    build:
      context: .
    privileged: true
    restart: always
    network_mode: host
    volumes:
      - 'resin-data:/data'
    labels:
      io.balena.features.kernel-modules: '1'
      io.balena.features.firmware: '1'
      io.balena.features.dbus: '1'
      io.balena.features.supervisor-api: '1'
      io.balena.features.balena-api: '1'
      io.balena.update.strategy: download-then-kill
      io.balena.update.handover-timeout: ''

Applications with multiple containers should include a Dockerfile or package.json in each service directory. A docker-compose.yml file will need to be defined at the root of the repository, as discussed in our multicontainer documentation.

You can also include a .dockerignore file with your project if you wish the builder to ignore certain files.

NOTE: You don't need to worry about ignoring .git as the builders already do this by default.

Dockerfile templates

One of the goals of balena is code portability and ease of use, so you can easily manage and deploy a whole fleet of different devices. This is why Docker containers were such a natural choice. However, there are cases where Dockerfiles fall short and can't easily target multiple different device architectures.

To allow our builders to build containers for multiple architectures from one code repository, we implemented simple Dockerfile templates.

It is now possible to define a Dockerfile.template file that looks like this:

FROM /%%BALENA_MACHINE_NAME%%-node

COPY package.json /package.json
RUN npm install

COPY src/ /usr/src/app
CMD ["node", "/usr/src/app/main.js"]

This template will build and deploy a Node.js project for any of the devices supported by balena, regardless of whether the device architecture is ARM or x86. In this example, you can see the build variable %%BALENA_MACHINE_NAME%%. This will be replaced by the machine name (i.e.: raspberry-pi) at build time. See below for a list of machine names.

The machine name is inferred from the device type of the application you are pushing to. So if you have an Intel Edison application, the machine name will be intel-edison and an i386 architecture base image will be built.

Note: You need to ensure that your dependencies and Node.js modules are also multi-architecture, otherwise you will have a bad time.

Currently our builder supports the following build variables:

Variable Name Description
BALENA_MACHINE_NAME The name of the yocto machine this board is base on. It is the name that you will see in most of the balena Docker base images. This name helps us identify a specific BSP.
BALENA_ARCH The instruction set architecture for the base images associated with this device.

Note: If your application contains devices of different types, the %%BALENA_MACHINE_NAME%% build variable will not evaluate correctly for all devices. Your application containers are built once for all devices, and the %%BALENA_MACHINE_NAME%% variable will pull from the device type associated with the application, rather than the target device. In this scenario, you can use %%BALENA_ARCH%% to pull a base image that matches the shared architecture of the devices in your application.

If you want to see an example of build variables in action, have a look at this basic openssh example.

Here are the supported machine names and architectures:

Device Name BALENA_MACHINE_NAME BALENA_ARCH GitHub
Raspberry Pi (v1 and Zero) raspberry-pi rpi https://github.com/balena-os/balena-raspberrypi
Raspberry Pi 2 raspberry-pi2 armv7hf https://github.com/balena-os/balena-raspberrypi
Raspberry Pi 3 raspberrypi3 armv7hf https://github.com/balena-os/balena-raspberrypi
BeagleBone Black beaglebone-black armv7hf https://github.com/balena-os/balena-beaglebone
BeagleBone Green Wireless beaglebone-green-wifi armv7hf https://github.com/balena-os/balena-beaglebone
BeagleBone Green beaglebone-green armv7hf https://github.com/balena-os/balena-beaglebone
Intel Edison intel-edison i386 https://github.com/balena-os/balena-edison
Intel NUC intel-nuc amd64 https://github.com/balena-os/balena-intel
Jetson TX2 jetson-tx2 aarch64 https://github.com/balena-os/balena-jetson-tx2
Hummingboard hummingboard armv7hf https://github.com/balena-os/balena-fsl-arm
Nitrogen 6X nitrogen6x armv7hf ttps://github.com/balena-os/balena-fsl-arm
Parallella parallella armv7hf https://github.com/balena-os/balena-parallella
Samsung Artik 1020 artik10 armv7hf https://github.com/balena-os/balena-artik
Samsung Artik 530 artik530 armv7hf https://github.com/balena-os/balena-artik
Samsung Artik 530s 1G artik533s armv7hf https://github.com/balena-os/balena-artik
Samsung Artik 710 artik710 aarch64 https://github.com/balena-os/balena-artik710
RushUp Kitra 710 kitra710 aarch64 https://github.com/balena-os/balena-artik710
UpBoard up-board amd64 https://github.com/balena-os/balena-up-board
Technologic TS-4900 ts4900 armv7hf https://github.com/balena-os/balena-ts
Odroid C1/C1+ odroid-c1 armv7hf https://github.com/balena-os/balena-odroid
Odroid XU4 odroid-xu4 armv7hf https://github.com/balena-os/balena-odroid
Variscite DART-6UL imx6ul-var-dart armv7hf https://github.com/balena-os/balena-imx6ul-var-dart
Generic ARMv7-a HF generic-armv7ahf armv7hf https://github.com/balena-os/balena-generic
Generic AARCH64 (ARMv8) generic-aarch64 aarch64 https://github.com/balena-os/balena-generic

Init system

Enable the init system

Whatever you define as CMD in your Dockerfile will be PID 1 of the process tree in your container. It also means that this PID 1 process needs to know how to properly process UNIX signals, reap orphan zombie processes [1] and if it crashes, your whole container crashes, meaning you lose logs and debug info.

For these reasons we have built an init system into most of the balena base images listed here: Balena Base Images Wiki. The init system will handle signals, reap zombies and also properly handle udev hardware events correctly.

There are two ways of enabling the init system in your application. You can add the following environment variable in your Dockerfile:

# enable container init system.
ENV INITSYSTEM on

You can also enable the init system from the dashboard: navigate to the Service variables menu item on the left and add INITSYSTEM with a value of on. Enable init system

Once you have enabled your init system you should see something like this in your device logs: init system enabled in logs

You shouldn't need to make any adjustments to your code or CMD—it should just work out of the box. Note that if you are using our Debian or Fedora based images, then you should have systemd in your containers, whereas if you use one of our Alpine images you will have OpenRC as your init system.

Setting up a systemd service

In some cases its useful to set up a service that starts up when your container starts. To do this with systemd, make sure you have the init system enabled in your container as mentioned above. You can then create a basic service file in your code repository called my_service.service and add something like this:

[Unit]
Description=My Super Sweet Service

[Service]
EnvironmentFile=/etc/docker.env
Type=OneShot
ExecStart=/etc/init.d/my_super_sweet_service

[Install]
WantedBy=basic.target

Then by adding the following to your Dockerfile your service should be added/enabled on startup:

ENV INITSYSTEM on
COPY my_service.service /etc/systemd/system/my_service.service
RUN systemctl enable /etc/systemd/system/my_service.service

Check out https://www.freedesktop.org/software/systemd/man/systemd.service.html#Options if you need a different service type (OneShot is for services that exit once they're finished starting, e.g. daemons).

Node applications

Balena supports Node.js natively using the package.json file located in the root of the repository to determine how to build and execute node applications.

When you push your code to your application's git endpoint the deploy server generates a container for the environment your device operates in, deploys your code to it and runs npm install to resolve npm dependencies, reporting progress to your terminal as it goes.

If the build executes successfully the container is shipped over to your device where the supervisor runs it in place of any previously running containers, using npm start to execute your code (note that if no start script is specified, it defaults to running node server.js.)

Node.js Example

A good example of this is the text-to-speech application - here's its package.json file*:

{
  "name": "text2speech",
  "description": "Simple balena app that uses Google's TTS endpoint",
  "repository": {
    "type": "git",
    "url": "https://github.com/balena-io/text2speech.git"
  },
  "scripts": {
    "preinstall": "bash deps.sh"
  },
  "version": "0.0.3",
  "dependencies": {
    "speaker": "~0.0.10",
    "request": "~2.22.0",
    "lame": "~1.0.2"
  },
  "engines": {
      "node": "0.10.22"
  }
}

Note: We don't specify a start script here which means node will default to running server.js.

We execute a bash script called deps.sh before npm install tries to satisfy the code's dependencies. Let's have a look at that:-

apt-get install -y alsa-utils libasound2-dev
mv sound_start /usr/bin/sound_start

These are shell commands that are run within the container on the build server which are configured such that dependencies are resolved for the target architecture not the build server's - this can be very useful for deploying non-javascript code or fulfilling package dependencies that your node code might require.

We use Raspbian as our contained operating system, so this scripts uses aptitude to install native packages before moving a script for our node code to use over to /usr/bin (the install scripts runs with root privileges within the container.)

Note: With plain Node.js project, our build server will automatically detect the specified node version in package.json file and build the container based on Docker image with satisfied node version installed. The default node version is 0.10.22 and it will be used if a node version is not specified. There will be an error if the specified node version is not in our registry. You can either try another node version or contact us to be supported. More details about Docker node images in our registry can be found here.

terminal-builder-window