Selectively running containers on a device

I figured I’d pose the question here first before descending down a rabbit hole. Here’s the hypothetical situation. I have two devices, deviceOne and deviceTwo, both of which are a Rpi3 running BalenaOS.

Both devices run the same BalenaCloud application, however each has a different functional use. Eg. deviceOne has a screen attached and deviceTwo reads a sensor. I have two services that run as part of the app, let’s call them kiosk and sensor. In theory, both devices run both services, each in a separate container. However, there’s no real need for deviceTwo to run the kiosk service as it has no screen attached, and vice versa for deviceOne. To save on resources, I’d like to selectively decide which service to run on each device, probably using a device ENV variable.

My thought process was this:

  • create a third service and call it something like boot-manager, have this service depend_on kiosk and sensor to ensure it launches last. When the container runs, run a script that sends a request to the supervisor API stopping the container that isn’t required (determined by reading of the device ENV, eg. RUN_KIOSK=0.

Is there a better way to do this? How do people usually tackle this problem?

Thanks!

Thanks for your question. I’m currently discussing this with a colleague and we will shortly have a reply laying out some options to consider.

Thanks for your patience! After discussing it thoroughly and testing on devices, I believe we have some good options for this specific case (and at the end, we discuss why this issue has not come up before, or doesn’t appear to have come up, from searching the forums).

Idea 1:

Do a kind of inverse of what you describe: Have the services optionally stop themselves on start after checking the env var, no need for a third manager service or interfacing with balena engine, etc. You should have restart: "no" in the service definition in docker-compose.yml, something like this:

version: '2'
services:
  kiosk:
    restart: "no"
    build: ./kiosk
  sensor:
    restart: "no"
    build: ./sensor

and then in the service, you’d have, top level in the entrypoint script (say we were using nodejs):

if (process.env['IS_SENSOR']) {
    // code to run service ...
} else {
    console.log('sensor not starting, did not find env var');
}

Idea 2:

Of course if you wanted to make a manager service for the sake of more control (I don’t know whether this is useful to you as you go on, or whether the simplest solution is best), you could take a look at the supervisor API [1].

Idea 3:

A final option is simply to deploy two separate applications. This would save the unused disk-space / network bandwidth of downloading and storing container images which won’t run. I would use a setup like you requested initially only in the case that I wanted to swap a device between sensor and kiosk mode in the field. If it’s determined when they’re shipped, then better to simply separate at the layer of application. This is the most common pattern and likely is used for this reason in preference to runtime “polymorphism”.

Let us know if you have any questions subsequent on this! We’ll be glad to assist.

[1] https://github.com/balena-io/balena-supervisor/blob/master/docs/API.md#service-actions

@dt-rush thank you for the in-depth description. I understand this is a unique use-case, but the polymorphism is a necessity to manage a complex fleet of devices that are spread between multiple sites / customers. Updates should be infrequent, and the devices stream data to the cloud as opposed to storing it, so the disk usage and/or bandwidth are not a huge concern when compared to the complexity of the task of managing the devices.

I like the approach of Idea 1, however I have a question: it’s my understanding that the service will not start automatically if you have restart: no set in the docker-compose. How then is it expected to start the service in the entry point script. In other words, will the entry point script still run if the container is instantiated as “stopped”? Or am I misunderstanding?

Hi @craigson , the restart: no will not instantiate the container in “stopped”. That field just means the container won’t be restarted in the case that its PID 1 process exits. So it will always start. You can also probably use restart: on-failure which would only restart the container if your process exited for some failed reason, but if it was exit 0 then it would just stop.

Thanks @shaunmulligan that makes more sense to me. Then having the restart: "no" only really makes sense if I’m stopping the container itself, correct? In other words, If I were to use the supervisor API to stop the container, I don’t want it restarting itself.

According to Idea 1, running code in the entry point would simply stop a nodejs server from running. If my Kiosk service was a container running a webkit browser, I’d need to stop the actual container from running to conserve the resources, wouldn’t that be correct?

The Supervisor API stop-service call [1] will stop a service for good, no need to inhibit restarts.

As to your second question, I’m not quite sure what you mean by “running code in the entry point would simply stop a nodejs server from running”. If the node script ends, the container (“service”) will terminate, since the entrypoint script has exited. That’s why in the example above, we only run the code for the service if its env var is present, otherwise we log and exit, thus terminating the container:

if (process.env['IS_SENSOR']) {
    // code to run service ...
} else {
    console.log('sensor not starting, did not find env var');
}

If you use this method, you will need to inhibit restarts.

I hope this helps clarify the situation! Let us know if you have any further questions and we’ll be glad to assist.

[1] https://github.com/balena-io/balena-supervisor/blob/master/docs/API.md#stop-a-service-post-v2applicationsappidstop-service

Yea, that makes sense - thank you for clarifying!