Skip to content

feat(bookworm): take Node.js from the official image instead of apt - #518

Merged
sidey79 merged 3 commits into
devfrom
feat/nodejs-from-official-image
Sep 25, 2026
Merged

sidey79 merged 3 commits into
devfrom
feat/nodejs-from-official-image

Conversation

@sidey79

@sidey79 sidey79 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Why

The full image currently installs nodejs from Debian bookworm, which means Node v18.20.4 — end-of-life since April 2025, and bookworm will never ship anything newer. Copying the runtime out of node:22-bookworm-slim gives Node 22, supported until April 2027, and keeps the version under Renovate's control like every other pinned image.

The catch: platform coverage

No Node image is published for linux/386, and from Node 24 on arm/v7 is gone too:

amd64 arm/v7 arm64 386
node:26 / 25 / 24 ✅ ❌ ✅ ❌
node:22 ✅ ✅ ✅ ❌
Debian nodejs ✅ ✅ ✅ ✅

Node 22 therefore covers three of the four platforms the full image was built for. Rather than carrying an apt fallback for the one remaining platform — which would pin it to Node 18 anyway — the full image stops being built for linux/386.

The minimal image is unaffected and keeps all four platforms, linux/386 included. It is built by a separate step (target: with-fhem) and contains no Node.js at all.

Two things that a COPY does not give you

/usr/local is not free real estate. The base image is perl:5.40.5-bookworm, and perl keeps its own installation there (/usr/local/bin/perl, /usr/local/lib/perl5, plus cpanm and cpm). A blanket COPY --from=node /usr/local /usr/local would wreck the perl runtime. Only /usr/local/bin/node and /usr/local/lib/node_modules are copied, and npm/npx are re-linked.

A COPY resolves no dependencies. Node links against libatomic on 32-bit arm. The deb used to pull that in automatically; without it, linux/arm/v7 fails at runtime:

/usr/local/bin/node: error while loading shared libraries:
libatomic.so.1: cannot open shared object file: No such file or directory

libatomic1 is now installed explicitly in that layer.

Verification

Built the copy against the real perl base image per platform:

platform result
linux/arm64 node=v22.23.3 npm=10.9.9 perl=5.040005
linux/arm/v7 failed on libatomic, then node=v22.23.3 npm=10.9.9 perl=5.040005

And applied the new layer to the current full image to check nothing else broke:

node=v22.23.3  npm=10.9.9  npx=10.9.9
perl=5.038005  Python 3.11.2
  GD              OK
  Inline::Python  OK
  DBD::SQLite     OK
  pychromecast   OK

Inline::Python and pychromecast matter here: Python stays on apt precisely because Inline::Python is linked against Debian's libpython3.11.so.1.0 and the 41 C extensions in dist-packages are bound to that ABI. This PR does not touch the Python layer.

Both Dockerfiles pass docker buildx build --check with no warnings.

What is not verified

No full multi-platform image was built end to end from here — that only happens in CI. The optional npm packages (alexa-fhem, homebridge-fhem, gassistant-fhem, tradfri-fhem) are guarded by IMAGE_LAYER_NODEJS_EXT, which defaults to "0" and is never set by the workflow, so CI does not exercise them. Anyone building with that flag moves from Node 18 to Node 22 across two majors and should test those packages.

Pre-existing bug surfaced by this PR: unconfigured packages on arm/v7 threaded

The first CI run failed on linux/arm/v7 (threaded) with exit code: 100 in the nodejs layer — but not because of libatomic1. apt tried to finish configuring packages left over from the extended layer:

Iconv.c: loadable library and perl binaries are mismatched (got first handshake key 0xa500080, needed 0xa400080)
dpkg: error processing package x11-common (--configure):
 installed x11-common package post-installation script subprocess returned error exit status 1

PERL5LIB points at XS modules built for the image perl 5.40 in /usr/local. dpkg maintainer scripts run Debian's /usr/bin/perl 5.36, which on threaded arm/v7 has the same archname (arm-linux-gnueabihf-thread-multi-64int) and therefore loads the image's Text::Iconv. The extended layer only runs set -x without -e, so this has been silently swallowed. The currently published image is affected:

ghcr.io/fhem/fhem-docker:5-threaded-bookworm arm/v7 amd64
x11-common half-configured installed
unconfigured ffmpeg, mplayer, libsdl2, libavdevice59, libice6, libaudio2 … —

The fix unsets PERL5LIB inside the three RUN blocks that call apt after it is set (extended, python, nodejs). ENV stays as it is, so FHEM still gets it at runtime.

Reproduced on linux/arm/v7 with the threaded perl base image plus the core lib from the published image:

VORHER  exit=100 Status: install ok half-configured | Iconv.c: loadable library and perl binaries are mismatched
NACHHER exit=0   Status: install ok installed       | audit=0

On the published image, dpkg --configure -a without PERL5LIB repairs every pending package and ffmpeg runs again.

The same leak applies to packages installed at container start through the deprecated APT_PKGS in entry.sh; that path is not changed here.

Note

Only the bookworm variants are changed; the bullseye images are legacy.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TQUFMNHUonVQShZWDP3vA5

sidey79 and others added 2 commits September 24, 2026 17:31
The full image is about to take its Node.js runtime from the official node
image, and that image is not published for linux/386 -- no Node major ever
was. Keeping 386 in the full image would mean carrying an apt fallback for
that one platform, which would pin it to Debian's Node 18.

The minimal image is built by a separate step and keeps all four platforms,
including linux/386. Only the bundled-runtime image loses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQUFMNHUonVQShZWDP3vA5
Debian bookworm ships Node 18, which reached end-of-life in April 2025 and
will never be updated there. Copying the runtime out of node:22-bookworm-slim
gives Node 22 (supported until April 2027) and drops the "npm install -g
npm@latest" step, because that image already carries a current npm.

Only /usr/local/bin/node and /usr/local/lib/node_modules are copied. Copying
all of /usr/local would break the image: the perl base image keeps perl,
cpanm and cpm in the very same prefix.

libatomic1 is installed explicitly. Node links against it on 32-bit arm, and
where the deb used to pull it in as a package dependency, a COPY resolves no
dependencies at all -- without it node dies on linux/arm/v7 with
"libatomic.so.1: cannot open shared object file".

Verified by building the copy against the perl base image for linux/arm64 and
linux/arm/v7 (node v22.23.3, npm 10.9.9, perl intact on both) and by applying
the new layer to the current full image, where perl, GD, DBD::SQLite,
Inline::Python, Python 3.11 and pychromecast all keep working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQUFMNHUonVQShZWDP3vA5
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

CPAN Build Report

✅ All CPAN builds are fine — 6 of 6 image/platform combinations verified successfully.

Per image details
Image Platform Result Details
-bookworm 386 ✅ ok –
-bookworm arm/v7 ✅ ok –
-bookworm arm64 ✅ ok –
-threaded-bookworm 386 ✅ ok –
-threaded-bookworm arm/v7 ✅ ok –
-threaded-bookworm arm64 ✅ ok –

Full inventories, logs and per-image reports are available as workflow artifacts of the workflow run.

From the fhem-core stage on, PERL5LIB points at XS modules that were built
for the image perl in /usr/local (5.40). dpkg maintainer scripts run
Debian's /usr/bin/perl (5.36) instead. Both share the same archname
(arm-linux-gnueabihf-thread-multi-64int on the threaded arm/v7 image), so the
system perl loads the image's Text::Iconv and aborts:

  Iconv.c: loadable library and perl binaries are mismatched

In the extended layer this made x11-common's postinst fail on linux/arm/v7
threaded. That RUN only uses "set -x" without "-e", so the failure was
swallowed: the published ghcr.io/fhem/fhem-docker:5-threaded-bookworm for
arm/v7 ships x11-common half-configured and ffmpeg, libsdl2, libavdevice59,
libice6, libaudio2 and mplayer unconfigured. The new nodejs layer runs with
"set -e", its apt-get tries to finish those pending configurations, and the
build now fails with exit code 100.

PERL5LIB is unset inside the three RUN blocks that call apt after it is set
(extended, python, nodejs). ENV is untouched, so FHEM still sees it at
runtime.

Reproduced on linux/arm/v7 with the threaded perl base image and the core
lib from the published image: installing x11-common with PERL5LIB set exits
100 and leaves it half-configured; with PERL5LIB unset it exits 0, the
package is installed and dpkg --audit is empty. On the published image,
"dpkg --configure -a" without PERL5LIB repairs every pending package and
ffmpeg runs again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQUFMNHUonVQShZWDP3vA5
@sidey79
sidey79 merged commit 6a239ca into dev Sep 25, 2026
31 checks passed
sidey79 added a commit that referenced this pull request Sep 25, 2026
## Why

`aptInstall()` runs `apt-get install` with `PERL5LIB` still pointing at
the image's XS modules. On the published threaded image, Debian's system
perl and the image perl share the same archname on `linux/386` and
`linux/arm/v7`, so the system perl loads the image's `Text::Iconv` and
the dpkg maintainer scripts abort:

```
Iconv.c: loadable library and perl binaries are mismatched
dpkg: error processing package x11-common (--configure)
```

Reproduced against the published
`ghcr.io/fhem/fhem-docker:5-threaded-bookworm`:

| platform (threaded) | `aptInstall()` result |
|---|---|
| linux/amd64 | installs cleanly |
| linux/arm64 | installs cleanly |
| linux/arm/v7 | `exit=100`, half-configured |
| linux/386 | `exit=100`, half-configured |

`APT_PKGS` is already documented as deprecated. Rather than carrying the
same `PERL5LIB` workaround used at build time (see #518) into a runtime
code path for a feature nobody is meant to rely on anymore, `APT_PKGS`
now only prints a notice and no longer calls `aptInstall()` or `apt-get`
at all.

## What stays untouched

`aptInstall()` itself is unchanged, including its existing bats unit
tests. Nothing else calls it after this change, but removing the
function is a separate decision from disconnecting `APT_PKGS` — this PR
does only what was asked.

## Verification

Sourced `entry.sh` with `APT_PKGS` set and `aptInstall()` replaced by a
probe that fails if called:

```
INFO:  APT_PKGS no longer installs packages in the running container.
ERGEBNIS: initialPackageSetup lief durch, aptInstall wurde NICHT aufgerufen
```

`initialPackageSetup()` completes without invoking `aptInstall()`, and
the notice is printed instead. `bash -n src/entry.sh` passes.

## Not covered

`CPAN_PKGS`, `PIP_PKGS` and `NPM_PKGS` still perform their installs and
are unaffected. `pip3 install` and `npm install -g` do not touch
`PERL5LIB`-dependent dpkg maintainer scripts, so they are not known to
share this bug.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01TQUFMNHUonVQShZWDP3vA5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant