Self-Updating IoT apps on the Raspberry Pi using Docker

Romanas Sonkinas
IMONT Technologies
4 min readMay 15, 2017

--

Here at IMONT Technologies we’ve been working on a problem that pains so many IoT solutions everywhere — how to easily update your software when it is out in the field. Imagine a situation — you have a bunch of Raspberry Pi-style micro computers deployed that run your software, and you want to release updates to it. One option is to get it into the official Raspbian repositories, but that would be time consuming and you’d be beholden to Raspbian’s release cycle — not to mention you’d need to somehow make sure all of those PIs would actually pull updates in a timely fashion.

Since we were already using Docker for many things here at IMONT, we figured it would be great to be able to deploy our in-the-field (read: not in the cloud) software the same way you’d expect to roll out software updates to some cloud app. Fortunately the Raspberry Pi (among others) has now received official Docker support, which means that it’s relatively straightforward to use the same techniques to deploy apps to PIs as you would use when deploying them to the cloud.

Let’s start with the basics…

Installing Docker on the Pi

Assuming you already have an up-to-date version of Raspbian, it is actually as easy as following the official Docker installation instructions, i.e.:

pi@raspberrypi:~ $ curl -sSL https://get.docker.com/ | sh

After following some prompts, you should have Docker up-and-running. You can verify your installation by doing running “docker ps”:

pi@raspberrypi:~ $ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

If you get this or similar output, Docker is successfully installed. One useful thing is to add the “pi” user to the docker group, which will allow invoking docker commands without sudo:

pi@raspberrypi:~ $ sudo usermod -G docker pi

Installing Watchtower

Watchtower is a pretty simple and cool Docker app that monitors your running containers for any changes — i.e. it would detect that a newer version is installed into the local or remote repo and automatically upgrade your running app. Although by default it is configured to pull changes from a remote repository, we’ll disable that and make it work in “offline” mode. The reason for that is we want to be in full control of when upgrades are happening, and to which version we upgrade, as opposed to it always pulling the latest one from Docker Hub.

We’ll be using systemd to configure automatic startup of watchtower by creating a config file like this:

[Unit]
Description=Watchtower container
Requires=network-online.target docker.service
After=docker.service
[Service]
Restart=never
ExecStartPre=-/usr/bin/docker rm -f watchtower
ExecStart=/usr/bin/docker run --name watchtower --rm -v /var/run/docker.sock:/var/run/docker.sock v2tec/watchtower:armhf-latest --no-pull -i 30 --cleanup
ExecStop=/usr/bin/docker stop -t 2 watchtower
[Install]
WantedBy=default.target

Now all we need to do is put it in /etc/systemd/system/watchtower.service and enable it like this:

pi@raspberrypi:~ $ sudo systemctl enable watchtower.service
pi@raspberrypi:~ $ sudo systemctl start watchtower.service

After which you should see it in the list of running containers:

pi@raspberrypi:~ $ docker psCONTAINER ID        IMAGE                           COMMAND                  CREATED             STATUS              PORTS               NAMEScef8390df0e0        v2tec/watchtower:armhf-latest   "/watchtower --no-..."   3 minutes ago       Up 3 minutes                            watchtower

Configuring our app

Now for the cool part — how to enable our app to upgrade itself when we push some new software out to it. The push mechanism itself is out of scope for this article — at IMONT we maintain a secured peer-2-peer connection to our running container and are able to send it commands that way, but you might have a channel open to, say, Amazon IoT or some bespoke solution and issue commands via that.

The important thing is to allow our app (running in a Docker container) access to Docker itself. We do that in exactly the same way we did with wachtower — by mounting the Docker socket into it. Here’s a sample systemd config file, which you can adapt as required:

[Unit]
Description=My App
Requires=network-online.target docker.service
After=docker.service
[Service]
# Don't restart, conflicts with Watchtower
Restart=never
ExecStartPre=-/usr/bin/docker rm -f my-app
ExecStart=/usr/bin/docker run --name my-app -v /var/run/docker.sock:/var/run/docker.sock -t my-app-container-name
ExecStop=/usr/bin/docker stop -t 2 my-app
[Install]
WantedBy=default.target

Note that unless your container is hosted on Docker hub, you’ll need to install it into the local Docker repo manually for the first time (i.e. docker load -i <my-app.tar>

The self-updating app code

Once we’ve started our application and it has access to the Docker socket (via /var/run/docker.sock), it’s simply a matter picking a Docker client library for the language you use and writing some code to download and install a container. We do it with Java, but the code for any other language would be pretty much equivalent. We use docker-java. Here’s some sample code to download a Docker image from a URL location and install into the local repo:

We use RxJava for all our code — which I highly recommend!

// docker-java requires no configuration
dockerClient = DockerClientBuilder.getInstance().build();
private Observable<File> download(final URL url) {
return Observable.fromCallable(() -> {
ReadableByteChannel rbc = Channels.newChannel(url.openStream());
File outFile = File.createTempFile("docker", "download");
outFile.deleteOnExit();

FileOutputStream fos = new FileOutputStream(outFile);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);

return outFile;
});
}
private Observable<Boolean> install(final File file) {
return Observable.fromCallable(() -> {
try (FileInputStream fs = new FileInputStream(file)) {
dockerClient.loadImageCmd(fs).exec();
}
return true;
});
}
// And all together now
download(someUrl).flatMap(this::install).subscribe(result -> {
logger.info("Installed? {}", result);
});

After you’ve installed the archive, watchtower should detect a change in your local repository and restart your running application. Voila!

Note: You can upgrade Watchtower itself in the same way.

--

--