Getting Started

Getting Started on the Raspberry Pi 3 B/B+

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 balena.io/os 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.

Currently the CLI is a node.js based command line tool which requires that our system has the following dependencies installed and in our path:

Once we have those setup we can install balena CLI using npm:

$ npm install --global --production --unsafe-perm balena-cli

Note: Depending on you node.js installation, you may need to use administrative privileges to install the CLI.

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
Done!

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.

[connection]
id=my-wifi
type=wifi

[wifi]
mode=infrastructure
ssid=I_Love_Unicorns

[wifi-security]
auth-alg=open
key-mgmt=wpa-psk
psk=superSecretPassword

[ipv4]
method=auto

[ipv6]
addr-gen-mode=stable-privacy
method=auto

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 the CLI provides a handy flashing utility, you can however flash this image using etcher.io or dd if you wish.

Flash SD card

To get flashing, just point the balena local flash command to the image we just downloaded and follow the prompts. If you hate prompts, the CLI also allows you to skip them, check the balena CLI docs on how to do this.

NOTE: balena local flash requires administrative privileges because it needs to access the SD card.

$ sudo balena local flash ~/Downloads/balena.img
Password:
x No available drives were detected, plug one in!

Once you plug in your SD card, the CLI should detect it and show you the following selection dialog. Make sure to select the correct drive if you have a few listed, as this will completely write over the drive.

$ sudo balena local flash ~/Downloads/balena.img
Password:
? Select drive (Use arrow keys)
❯ /dev/disk3 (7.9 GB) - STORAGE DEVICE

Once you are happy you have selected the correct drive, hit enter and wait while your new OS is written to the drive. It should only take about 3 minutes, depending on the quality of your drive, so this is a great time to go grab a caffeinated beverage.

$ sudo balena local flash ~/Downloads/balena.img
? Select drive /dev/disk3 (7.9 GB) - STORAGE DEVICE
? This will erase the selected drive. Are you sure? Yes
Flashing [========================] 100% eta 0s
Validating [========================] 100% eta 0s

Note: Remember to safely remove your SD card before pulling it out.

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 192.168.1.111 (192.168.1.111): 56 data bytes
64 bytes from 192.168.1.111: icmp_seq=0 ttl=64 time=103.674 ms
64 bytes from 192.168.1.111: icmp_seq=1 ttl=64 time=9.723 ms
^C
--- 192.168.1.111 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 shaun 4.14.79 #1 SMP Fri Jan 18 03:56:49 UTC 2019 armv7l armv7l armv7l GNU/Linux
root@shaun:~# balena-engine info
Containers: 1
 Running: 1
 Paused: 0
 Stopped: 0
Images: 1
Server Version: 17.12.0-dev
Storage Driver: aufs
 Root Dir: /var/lib/docker/aufs
 Backing Filesystem: extfs
 Dirs: 42
 Dirperm1 Supported: true
Logging Driver: journald
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host ipvlan null
 Log: journald json-file
Swarm: 
 NodeID: 
 Is Manager: false
 Node Address: 
Runtimes: bare runc
Default Runtime: runc
Init Binary: balena-engine-init
containerd version: 
runc version: 13e66eedaddfbfeda2a73d23701000e4e63b5471
init version: N/A (expected: )
Kernel Version: 4.14.79
Operating System: balenaOS 2.29.2+rev1
OSType: linux
Architecture: armv7l
CPUs: 4
Total Memory: 975.7MiB
Name: shaun
ID: AMKY:KLLA:DTPW:S3WA:5ZWV:EXRG:DSWT:ZNL2:BDIW:CDMS:SXJM:QB3Y
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: true
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

Running your first Container

Clone a demo Project

$ git clone https://github.com/balena-io-projects/multicontainer-getting-started.git

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

OR

$ ssh root@mydevice.local -p22222

To ssh into a particular container:

$ sudo balena local ssh mydevice.local

OR

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

Going Further

Advanced Settings

Either mount the SD card 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 SD card.

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/raspberrypi3-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 192.168.1.111
[Info]    Starting build on device 192.168.1.111
[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="http://alpinelinux.org"
[Logs]    [1/24/2019, 3:56:17 PM] [main] BUG_REPORT_URL="http://bugs.alpinelinux.org"
[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.

//main.js
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/raspberrypi3-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 <shaun@balena.io>",
  "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 <shaun@balena.io>",
  "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.

//main.js

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/raspberrypi3-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.