Skip to content

Instantly share code, notes, and snippets.

@Paraphraser
Last active May 6, 2024 12:25
Show Gist options
  • Save Paraphraser/6a6628c3a873047cefa5eeda21502cf0 to your computer and use it in GitHub Desktop.
Save Paraphraser/6a6628c3a873047cefa5eeda21502cf0 to your computer and use it in GitHub Desktop.
IOTstack tutorial: Upgrading the Debian-based Node-RED DockerHub image to use node.js version 18

IOTstack tutorial: Debian Node-RED + node.js 18

Scenario

A thread on the IOTstack Discord channel can be summarised like this:

  1. The DockerHub nodered/node-red releases page includes tags for both Alpine and Debian-based containers.

  2. 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
  3. Assume a requirement to add node-red-contrib-homekit-bridged to the container.

  4. The add-on node has a dependency on Node.js version 18 or later.

  5. 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!

Caveat

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.

service definition

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:

  1. Deactivate the existing build: clause by prepending x-.
  2. 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-based latest container. You can pass any valid version number for Node.js but keep the above caveat in mind. You can also pass the value latest which, at the time of writing, implies Node.js 22.1.0.

Dockerfile

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:

  1. 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.

  2. 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 manager apt.

  3. 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.

Reference version

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

Build the container

Run these commands:

$ cd ~/IOTstack
$ docker-compose build --no-cache --pull nodered
$ docker-compose up -d nodered
$ docker system prune -f

Check your work

Once the rebuilt container is running:

  1. 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)"
    
  2. 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

Other notes

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:

  1. Edit your flows to adopt localhost:«externalport» syntax. Examples:

    • Refer to Mosquitto as localhost:1883
    • Refer to InfluxDB as localhost:8086
  2. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment