Getting Started

Getting Started on the Balena Fin (CM3)

In balenaOS all application logic and services are encapsulated in Docker containers. In this getting started guide we will walk you through setting up one of our pre-built development OS images and creating a simple application container. In the guide we will use the balena CLI tool to make things super easy.

Download an Image

To get a balenaOS device setup, we will first need to flash a system image on to the device, so head over to and grab the development OS for your board. Currently the OS supports 25 different boards and several different architectures. See the Supported Boards section for more details.

Once the download is finished, make sure to decompress it and keep the resulting balena.img somewhere safe, we will need it very soon!

Install the Balena CLI

The CLI is a collection of utilities which helps us to develop balenaOS based application containers.

Install the CLI with the following instructions, available here.

Configure the Image

To allow balenaOS images to be easily configurable before boot, some key config files are added to boot partition. In this step we will use the CLI to configure the network, set our hostname to mydevice and disable persistent logging, because we don’t want to kill our poor flash storage with excessive writes.

$ sudo balena local configure ~/Downloads/balena.img
? Network SSID I_Love_Unicorns
? Network Key superSecretPassword
? Do you want to set advanced settings? Yes
? Device Hostname mydevice
? Do you want to enable persistent logging? no

If you are not using the CLI, you will need to mount the boot partition of the image and edit the configuration manually.

Edit /boot/config.json so it looks like this:

  "persistentLogging": false,
  "hostname": "mydevice",

And create a file in /boot/system-connections called my-wifi with the following content and the ssid and psk values replaced as needed.






If you only want to use an ethernet connection on your device, you don't need to add anything. The device will automatically set up an ethernet connection by default.

Get the Device Up and Running

Okay, so now we have a fully configured image ready to go, so let’s flash and boot this baby. For this step we recommend balenaEtcher a handy flashing utility. You can, however, flash this image using dd or any other SD card flashing utility, if you wish.

Flash internal MMC

Open balenaEtcher and use the blue "Select Image" button to find the disk image you downloaded earlier. Once you have selected the image you want to flash, insert your SD card or connect your device (in the case of a balenaFin) to your laptop and flash the image.

Boot the device

Now power on and boot up your device, after about 10 seconds or so your device should be up and connected to your local network, you should see it broadcasting itself as mydevice.local. To check this, let’s try ping the device.

$ ping mydevice.local
PING ( 56 data bytes
64 bytes from icmp_seq=0 ttl=64 time=103.674 ms
64 bytes from icmp_seq=1 ttl=64 time=9.723 ms
--- ping statistics ---
6 packets transmitted, 6 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 7.378/24.032/103.674/35.626 ms

Now if we want to poke around a bit inside balenaOS we can just ssh in with:

$ sudo balena local ssh mydevice.local --host
root@mydevice:~# uname -a
Linux mydevice 4.14.79 #1 SMP Fri Jan 18 03:56:49 UTC 2019 armv7l armv7l armv7l GNU/Linux
root@mydevice:~# balena-engine version
 Version:	17.12.0-dev
 API version:	1.35
 Go version:	go1.9.7
 Git commit:	fe78e2c9a69313007c53c83fff4b5525fbc2ba45
 Built:	Mon Mar 11 15:28:35 2019
 OS/Arch:	linux/arm
 Experimental:	false
 Orchestrator:	swarm

  Version:	17.12.0-dev
  API version:	1.35 (minimum version 1.12)
  Go version:	go1.9.7
  Git commit:	fe78e2c9a69313007c53c83fff4b5525fbc2ba45
  Built:	Mon Mar 11 15:28:35 2019
  OS/Arch:	linux/arm
  Experimental:	true

Running your first Container

Clone a demo Project

$ git clone

Get some Containers Running

To launch containers on our device, we will use the balena push command. First we need to change directory into the project we cloned down and then issue the following command, replacing <DEVICE_IP> with the IP address of our device from above.

$ balena push <DEVICE_IP>

This command will use the image specified by the docker-compose.yml or Dockerfile in the root of your project directory. The build of this project will happen on your balenaOS device and once completed, the command will start up a container from that newly built image(s).

Poking Around balenaOS

To help explore balenaOS devices and application containers more easily, the balena CLI has an ssh command which will help you connect either to the HostOS or a running container on the device.

To ssh into the host:

$ sudo balena local ssh --host


$ ssh root@mydevice.local -p22222

To ssh into a particular container:

$ sudo balena local ssh mydevice.local


$ ssh root@mydevice.local -p22222
root@mydevice:~# balena-engine exec -it myapp bash

Going Further

Advanced Settings

Either mount the internal MMC and run:

$ sudo balena local configure /path/to/drive

And select y when asked if you want to add advanced settings.

Alternatively you can add “persistentLogging”: true to config.json in your boot partition of the internal MMC.

To Enable persistent logs in a running device, add “persistentLogging”: true to /mnt/boot/config.json and reboot.

The journal can be found at /var/log/journal/ which is bind mounted to root-overlay/var/log/journal in the resin-conf partition. When logging is not persistent, the logs can be found at /run/log/journal/ and this log is volatile so you will loose all logs when you power the device down.

Creating a Project from Scratch

Alright! So we have an awesome container machine up and running on our network. So let’s start pushing some application containers onto it. In this section we will do a quick walk through of setting up a Dockerfile and make a simple little node.js webserver.

To get started, let’s create a new project directory called “myapp” and create a new file called Dockerfile.

$ mkdir -p myapp && touch Dockerfile

Now we will create a minimal node.js container based on the slim Alpine Linux distro. We do this by adding the following lines to our Dockerfile.

FROM balenalib/fincm3-alpine-node
CMD ["cat", "/etc/os-release"]

The FROM tells Docker what our container will be based on. In this case an Alpine Linux userspace with just the bare essentials needed for the node.js runtime. The CMD just defines what our container runs on startup. In this case, it’s not very exciting yet.

Now to get our application running on our device we can use the balena push <DEVICE_IP> functionality.

$ balena push
[Info]    Starting build on device
[Info]    Creating default composition with source: .
[Build]   [main] Step 1/3 : FROM balenalib/raspberrypi3-alpine-node
[Build]   [main]  ---> b3c5b3f0b567
[Build]   [main] Step 2/3 : CMD ["cat", "/etc/os-release"]
[Build]   [main]  ---> Running in 9dd1bd4aa347
[Build]   [main] Removing intermediate container 9dd1bd4aa347
[Build]   [main]  ---> 11cdc034692b
[Build]   [main] Step 3/3 : LABEL "io.resin.local.image"='1' "io.resin.local.service"='main'
[Build]   [main]  ---> Running in 96f0a2c9d544
[Build]   [main] Removing intermediate container 96f0a2c9d544
[Build]   [main]  ---> a739c6e5e44d
[Build]   [main] Successfully built a739c6e5e44d
[Build]   [main] Successfully tagged local_image_main:latest

[Info]    Streaming device logs...
[Logs]    [1/24/2019, 3:56:15 PM] Killing service 'main sha256:2f1a921a8aca15a231669d42ee8db7fb6c1cc8659d1af89fe10e988920145d66'
[Logs]    [1/24/2019, 3:56:15 PM] Killed service 'main sha256:2f1a921a8aca15a231669d42ee8db7fb6c1cc8659d1af89fe10e988920145d66'
[Logs]    [1/24/2019, 3:56:16 PM] Installing service 'main sha256:a739c6e5e44d72ba38959a2a72390e1317118a8c571c076d4130bc4eade2e9ab'
[Logs]    [1/24/2019, 3:56:17 PM] Installed service 'main sha256:a739c6e5e44d72ba38959a2a72390e1317118a8c571c076d4130bc4eade2e9ab'
[Logs]    [1/24/2019, 3:56:17 PM] Starting service 'main sha256:a739c6e5e44d72ba38959a2a72390e1317118a8c571c076d4130bc4eade2e9ab'
[Logs]    [1/24/2019, 3:56:18 PM] Started service 'main sha256:a739c6e5e44d72ba38959a2a72390e1317118a8c571c076d4130bc4eade2e9ab'
[Logs]    [1/24/2019, 3:56:17 PM] [main] NAME="Alpine Linux"
[Logs]    [1/24/2019, 3:56:17 PM] [main] ID=alpine
[Logs]    [1/24/2019, 3:56:17 PM] [main] VERSION_ID=3.8.2
[Logs]    [1/24/2019, 3:56:17 PM] [main] PRETTY_NAME="Alpine Linux v3.8"
[Logs]    [1/24/2019, 3:56:17 PM] [main] HOME_URL=""
[Logs]    [1/24/2019, 3:56:17 PM] [main] BUG_REPORT_URL=""
[Logs]    [1/24/2019, 3:56:18 PM] Service exited 'main sha256:a739c6e5e44d72ba38959a2a72390e1317118a8c571c076d4130bc4eade2e9ab'
[Logs]    [1/24/2019, 3:56:19 PM] Restarting service 'main sha256:a739c6e5e44d72ba38959a2a72390e1317118a8c571c076d4130bc4eade2e9ab'

This command will start the build on your local balenaOS device from whatever you have in the current working directory. It will then start up all the containers and stream back the logs from each container to the terminal. We can see that for our local balenaOS device we have an app called [main] which will be created from an image called local_image_main and is associated to a container on our device called main_1_1. You will notice that the container keeps restarting over and over. This is due to the fact that the our main process of printing out the os-release file exits after running and by default our containers restart policy is to always restart containers.

So now that we are building, let’s start adding some actual code! We will just add main.js file in the root of our myapp directory.

console.log("Hey… I’m a node.js app running in a container!!");

We then make sure our Dockerfile copies this source file into our container context by replacing our current CMD ["cat","/etc/os-release"] in our Dockerfile with the following.

FROM balenalib/fincm3-alpine-node
WORKDIR /usr/src/app
COPY . .
CMD ["node", "main.js"]

This puts all the contents of our myapp directory into /usr/src/app in our running container and says we should start main.js when the container starts.

Alright, so we have a simple javascript container, but that’s pretty boring, let’s add some dependencies and complexity. To add dependencies in node.js we need a package.json, the easiest way to whip up one is to just run npm init in the root of our myapp directory. After a nice little interactive dialog we have the following package.json in directory.

  "name": "myapp",
  "version": "1.0.0",
  "description": "a simple hello world webserver",
  "main": "main.js",
  "scripts": {
    "test": "echo \"no tests yet\""
  "repository": {
    "type": "git",
    "url": "none"
  "author": "Shaun Mulligan <>",
  "license": "ISC"

Now it’s time to add some dependencies. For our little webserver, we will use the popular expressjs module. We can add it to the package.json after the "license": "ISC", so it now looks like this:

  "name": "myapp",
  "version": "1.0.0",
  "description": "a simple hello world webserver",
  "main": "main.js",
  "scripts": {
    "test": "echo \"no tests yet\""
  "repository": {
    "type": "git",
    "url": "none"
  "author": "Shaun Mulligan <>",
  "license": "ISC",
  "dependencies": {
    "express": "^4.14.0"

Now all we need to do is add a few more lines of javacript to our main.js and we are off to the races.


var express = require('express');
var app = express();

// reply to request with "Hello World!"
app.get('/', function (req, res) {
  res.send("Hello World, I'm a container running on balenaOS!");

//start a server on port 80 and log its start to our console
var server = app.listen(80, function () {

  var port = server.address().port;
  console.log("Hey… I’m a node.js server running in a container and listening on port: ", port);

Great, so now we are almost ready to go, but we want to make sure our dependency gets installed when we build. We then need to run a npm install in our build, so we add a few lines to our Dockerfile.

FROM balenalib/fincm3-alpine-node
WORKDIR /usr/src/app
COPY package.json package.json
RUN npm install
COPY . .
CMD ["node", "main.js"]

NOTE: Add node_modules to your .dockerignore file, otherwise your local modules might be copied to the device with the above Dockerfile, and they are likely the wrong architecture for your application!

We can now deploy our new webserver container again with:

$ balena push <DEVICE_IP>

You should now be able to point your web browser on your laptop to the IP address of your device and see the "Hello, World!" message.