A thread on the IOTstack Discord channel can be summarised like this:
-
The DockerHub
nodered/node-red
releases page includes tags for both Alpine and Debian-based containers. -
At the time of writing (2024-05-06), the support matrix is:
DockerHub tag latest
latest-debian
Container OS Alpine Debian OS Version 3.19.1 10 (buster) Node-RED version 3.1.9 3.1.9 Node.js version 18.20.2 16.20.2 -
Assume a requirement to add
node-red-contrib-homekit-bridged
to the container. -
The add-on node has a dependency on Node.js version 18 or later.
-
That requirement can only be satisfied with the Alpine-based container whereas the user's preference is to use the Debian-based container.
The question is:
Within the IOTstack framework, is it possible to upgrade the version of Node.js used by the Debian-based container so the requirement can be satisfied?
Short answer: yes!
I subscribe to the view that the wonderful folks who maintain Node-RED and prepare releases on DockerHub have good reasons for what they do. If they are building Debian containers based on Buster rather than Bullseye or Bookworm, I assume they know something I don't. Ditto if they deploy Node.js 16 on Buster instead of Node.js 18 or later.
So, follow this gist if you wish but you will need to accept all the responsibility for testing that Node.js 18 actually works in practice and doesn't fight with anything else in the container.
IOTstack's default service definition for Node-RED is:
nodered:
container_name: nodered
build:
context: ./services/nodered/.
args:
- DOCKERHUB_TAG=latest
- EXTRA_PACKAGES=
restart: unless-stopped
user: "0"
environment:
- TZ=${TZ:-Etc/UTC}
ports:
- "1880:1880"
volumes:
- ./volumes/nodered/data:/data
- ./volumes/nodered/ssh:/root/.ssh
Augment it like this:
nodered:
container_name: nodered
x-build:
context: ./services/nodered/.
args:
- DOCKERHUB_TAG=latest-18
- EXTRA_PACKAGES=
build:
context: ./services/nodered/.
dockerfile: Debian.Dockerfile
args:
- DOCKERHUB_TAG=latest-debian
- EXTRA_PACKAGES=
- NODEJS_VERSION=18.20.2
restart: unless-stopped
environment:
- TZ=${TZ:-Etc/UTC}
ports:
- "1880:1880"
user: "0"
volumes:
- ./volumes/nodered/data:/data
- ./volumes/nodered/ssh:/root/.ssh
In words:
- Deactivate the existing
build:
clause by prependingx-
. - Insert the new
build:
clause as shown above (seven lines).
If you have any EXTRA_PACKAGES
specified, you will need to make sure that you allow for any package-name differences between Alpine and Debian.
For example, here is my list of extra packages for Alpine:
- EXTRA_PACKAGES=mosquitto-clients bind-tools tcpdump tree
The mosquitto-clients
, tcpdump
and tree
packages have the same names in both package managers but bind-tools
is named dnsutils
in the Debian repositories. Thus my extra packages list for Debian needs to be:
- EXTRA_PACKAGES=mosquitto-clients dnsutils tcpdump tree
Notes:
- The reason for retaining the existing
build:
clause in its deactivated form is to make it easy for you to switch back and forth between the Debian and Alpine builds. - The reason for specifying
NODEJS_VERSION=18.20.2
is because that is the version of Node.js currently deployed with the Alpine-basedlatest
container. You can pass any valid version number for Node.js but keep the above caveat in mind. You can also pass the valuelatest
which, at the time of writing, implies Node.js 22.1.0.
Make a copy of the existing (Alpine) Dockerfile:
$ cd ~/IOTstack/services/nodered
$ cp Dockerfile Debian.Dockerfile
Note:
- The reason for making a copy is to preserve your existing Alpine-aware Dockerfile so you can easily switch back if you break something.
Open Debian.Dockerfile
in a text editor and make the following changes:
-
Find the line:
ENV EXTRA_PACKAGES=${EXTRA_PACKAGES}
After that line, insert the following lines:
# reference argument - defaults to latest ARG NODEJS_VERSION=latest ENV NODEJS_VERSION=${NODEJS_VERSION}
This change brings the value of
NODEJS_VERSION
which was specified in the service definition into the build context. -
Find the line:
RUN apk update && apk add --no-cache eudev-dev ${EXTRA_PACKAGES}
and replace that line with:
RUN apt update && apt install -y udev ${EXTRA_PACKAGES}
This change replaces the Alpine package manager
apk
with the Debian package managerapt
. -
Immediately after the line replaced in step 2, insert the following lines:
# update node.js RUN npm cache clean -f && npm install -g n && n ${NODEJS_VERSION}
This change adds the ability to upgrade Node.js.
Save your work.
Run:
$ head -30 Debian.Dockerfile
The expected output is:
# reference argument - omitted defaults to latest
ARG DOCKERHUB_TAG=latest
# Download base image
FROM nodered/node-red:${DOCKERHUB_TAG}
# reference argument - omitted defaults to null
ARG EXTRA_PACKAGES
ENV EXTRA_PACKAGES=${EXTRA_PACKAGES}
# reference argument - defaults to latest
ARG NODEJS_VERSION=latest
ENV NODEJS_VERSION=${NODEJS_VERSION}
# default user is node-red - need to be root to install packages
USER root
# install packages
RUN apt update && apt install -y udev ${EXTRA_PACKAGES}
# update node.js
RUN npm cache clean -f && npm install -g n && n ${NODEJS_VERSION}
# switch back to default user
USER node-red
# variable not needed inside running container
ENV EXTRA_PACKAGES=
# add-on nodes follow
Run these commands:
$ cd ~/IOTstack
$ docker-compose build --no-cache --pull nodered
$ docker-compose up -d nodered
$ docker system prune -f
Once the rebuilt container is running:
-
Confirm the operating system running inside the container:
$ docker exec nodered grep "PRETTY_NAME" /etc/os-release
The expected answer is:
PRETTY_NAME="Debian GNU/Linux 10 (buster)"
-
Confirm the versions of Node-RED and Node.js:
$ docker exec nodered npm version --json | jq -r '[.["node-red-docker"],.["node"]] | @tsv'
The expected answer is:
3.1.9 18.20.2
Interpretation:
- Node-RED version 3.1.9
- Node.js version 18.20.2
The node-red-contrib-homekit-bridged
add-on node needs to be able to see non-unicast traffic. That means the Node-RED container needs to run in host mode. Accordingly, the service definition needs to be changed like this:
x-ports:
- "1880:1880"
network_mode: host
When Node-RED runs in non-host mode, you refer to other non-host mode containers using «container»:«internalport»
syntax. Examples:
mosquitto:1883
influxdb:8086
You lose the ability to use that syntax when Node-RED is placed in host mode. You have two choices:
-
Edit your flows to adopt
localhost:«externalport»
syntax. Examples:- Refer to Mosquitto as
localhost:1883
- Refer to InfluxDB as
localhost:8086
- Refer to Mosquitto as
-
Augment your service definition to define the non-host mode containers Node-RED needs to be able to reach:
extra_hosts: - "mosquitto:host-gateway" - "influxdb:host-gateway"
Technically, you also need to change to your flows to refer to the
«externalport»
rather than the«internalport»
. However, as Mosquitto and InfluxDB use the same port numbers for both their external and internal ports, in practice you do not need to edit your flows.Nevertheless, the requirement to use the external port is something you need to keep in mind if any Node-RED flow needs to refer to a non-host mode container where external and internal port numbers are different.