Skip to main content

Das beste Node.js-Docker-Image auswählen

hero docker secrets

30. September 2022

0 Min. Lesezeit
How to Choose the Best and Secure Node.js Docker Image

Anmerkung der Redaktion:

Aktualisiert und um ein Chainguard-Distroless-Image für Node.js ergänzt.

(31. August 2023) Die empfohlene Node.js-Version, Beispiele und Ergebnisse der Schwachstellenscans wurden aktualisiert, um den aktuellen Node.js-LTS-Versionen zu entsprechen.

Die Wahl eines Node.js-Docker-Images mag nebensächlich erscheinen, doch Image-Größe und potenzielle Sicherheitslücken können sich erheblich auf Ihre CI/CD-Pipeline und Ihre Sicherheitslage auswirken. Wie wählen Sie also das beste Node.js-Docker-Image aus?

Die potenziellen Risiken von FROM node:latest oder einfach FROM node (ein Alias für Ersteres) werden leicht übersehen. Das gilt umso mehr, wenn Ihnen die allgemeinen Sicherheitsrisiken und die enorme Dateigröße, die dadurch in eine CI/CD-Pipeline gelangen, nicht bewusst sind.

Das folgende Beispiel für ein Node.js-Dockerfile wird in Tutorials und Blogbeiträgen zu Node.js-Docker-Images häufig als Referenz verwendet – doch dieses Dockerfile ist stark fehlerhaft und wird nicht empfohlen:

FROM node
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
CMD "npm" "start"

Ich habe bereits eine Schritt-für-Schritt-Anleitung zu 10 Best Practices für die Containerisierung von Node.js-Webanwendungen mit Docker veröffentlicht. Sie baut auf diesem Beispiel auf und verbessert es, um ein produktionsreifes Node.js-Docker-Image zu erstellen.

In diesem Beitrag verwenden wir das konstruierte Beispiel oben als Inhalt einer Dockerfile, um ein ideales Node.js-Docker-Image zu finden.

Ihre Möglichkeiten bei Node.js-Docker-Images

Beim Erstellen Ihres Node.js-Images stehen Ihnen tatsächlich zahlreiche Möglichkeiten zur Auswahl. Dazu gehören das offizielle Node.js-Docker-Image, das vom Node.js-Core-Team gepflegt wird, verschiedene Node.js-Image-Tags dieses Basis-Images und weitere Optionen: Sie können Ihre Node.js-Anwendung auf einem distroless-Image von Google oder Chainguard aufbauen oder ein minimales scratch-Image des Docker-Teams verwenden.

Welches dieser Node.js-Docker-Images ist für Sie am besten geeignet?

Sehen wir uns die Optionen einzeln an, um mehr über ihre Vorteile und potenziellen Risiken zu erfahren.

Anmerkung des Autors: Im Laufe dieses Artikels vergleiche ich eine Node.js-Version zu einem bestimmten Zeitpunkt, die zuletzt etwa im April 2024 veröffentlicht wurde und sich auf Node.js 22.1.0 bezieht.

Das standardmäßige Node-Image

Beginnen wir mit dem gepflegten node-Image. Es wird offiziell vom Node.js-Docker-Team gepflegt und umfasst mehrere Docker-Basis-Image-Tags, die verschiedenen zugrunde liegenden Distributionen (Debian, Ubuntu oder Alpine) und Versionen der Node.js-Laufzeitumgebung entsprechen. Außerdem gibt es spezifische Versions-Tags für CPU-Architekturen wie amd64 oder arm64x8 (den neuen Apple M1).

Die gängigsten node-Image-Tags für Debian-Distributionen, etwa bullseye oder bookworm, basieren ihrerseits auf buildpack-deps, das von einem anderen Team gepflegt wird.

Was passiert, wenn Sie Ihr Node.js-Docker-Image auf Grundlage dieses standardmäßigen node-Images erstellen und nur die npm-Abhängigkeit fastify hinzufügen? Der Einfachheit halber verwenden wir dieses Beispiel anstelle einer vollständigen Anwendungsbereitstellung.

FROM node
WORKDIR /app
RUN npm install fastify

Erstellen Sie das Image im selben Verzeichnis mit docker build --no-cache -t mynode . — in diesem Beispiel habe ich auch das node:latest-Image heruntergeladen, um den Größenunterschied zu zeigen. Das Ergebnis:

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
mynode       latest    6978fbd7640d   24 seconds ago   1.13GB
node         latest    2fb0552f149e   11 days ago   1.11GB
  • Wir haben keine Node.js-Laufzeitversion angegeben. Daher ist node ein Alias für node:latest, das derzeit auf Node.js Version 22.1.0 verweist.

  • Unser Docker-Image ist 1,13 GB groß.

  • Das node:latest-Basis-Image macht davon 1,11 GB aus.

Wie groß sind die Abhängigkeits- und Sicherheitslücken-Fußabdrücke dieses aktuellen Node.js-Images? Ein Container-Scan gibt Aufschluss. Wenn Sie mitmachen und dieselben Scans für Ihre Builds ausführen möchten, benötigen Sie ein kostenloses Snyk-Konto und müssen die Snyk-CLI installieren. Das geht mit dem einfachen Befehl `npm install -g snyk` oder anhand der Anleitung in unserer Dokumentation.

Wir führen snyk container test mynode --file=Dockerfile --exclude-app-vulns aus. Das ergibt Folgendes:

  • Insgesamt 413 Abhängigkeiten – dazu zählt jede erkannte Open-Source-Bibliothek, die der Betriebssystem-Paketmanager erfasst hat, beispielsweise curl/libcurl4, git/git-man oder imagemagick/imagemagick-6-common.

  • In diesen Abhängigkeiten wurden insgesamt 179 Sicherheitsprobleme gefunden, darunter Buffer Overflows, Use-after-Free-Fehler, Out-of-bounds-Writes und mehr.

  • Ältere Node.js-Images, etwa Node.js 20.5.1, wiesen verschiedene Sicherheitsprobleme auf, beispielsweise DNS-Rebinding, HTTP-Request-Smuggling und Configuration Hijacking.

Benötigen Sie wget, git oder curl tatsächlich in Ihrem Node.js-Image für die Anwendung? Insgesamt ist das kein schönes Bild: Hunderte Abhängigkeiten und Tools im Node.js-Docker-Image, denen Hunderte Sicherheitslücken zugeordnet sind, eröffnen zahlreiche potenzielle Angriffswege.

Node.js-Docker-Hub-Optionen: node:buster vs. node:bullseye vs. node:bookworm

Wenn Sie die verfügbaren Tags im Node.js-Docker-Hub-Repository durchsehen, finden Sie mehrere alternative Node.js-Image-Tags, darunter node:buster, node:bullseye und node:bookworm.

Alle drei Docker-Image-Tags basieren auf Debian-Distributionen. Das Image-Tag buster entspricht Debian 10, dessen Support-Ende zwischen August 2022 und 2024 liegt – es ist also keine gute Wahl. Das Image-Tag bullseye entspricht Debian 11, gilt als aktuelle „Old-Stable“-Version von Debian und erreicht voraussichtlich im Juni 2026 das Support-Ende. bookworm ist die aktuelle „Stable“-Version; zum Zeitpunkt der Veröffentlichung stand das Support-Ende noch nicht fest. Aufgrund des bisherigen Veröffentlichungsrhythmus lässt sich jedoch davon ausgehen, dass es irgendwann 2028 sein wird.

Anmerkung des Autors: Daher wird dringend empfohlen, alle neuen und bestehenden Node.js-Docker-Images von node:buster-Image-Tags auf node:bullseye, node:bookworm, oder andere geeignete Alternativen umzustellen.

Erstellen wir ein neues Node.js-Docker-Image auf Grundlage von:

FROM node:bookworm

Wenn Sie dieses Node.js-Docker-Image-Tag erstellen und mit den obigen Ergebnissen vergleichen, erhalten Sie exakt dieselbe Größe, Anzahl an Abhängigkeiten und Anzahl gefundener Sicherheitslücken. Der Grund: node, node:latest und node:bookworm verweisen alle auf dasselbe erstellte Node.js-Image-Tag.

Node.js-Image-Tag für schlankere Images

Das offizielle Node.js-Docker-Team pflegt außerdem ein Image-Tag, das ausdrücklich nur auf die Tools abzielt, die für eine funktionsfähige Node.js-Umgebung erforderlich sind.

Diese Node.js-Image-Tags werden als slim-Varianten bezeichnet, etwa node:bookworm-slim. Es gibt auch Tags für bestimmte Node.js-Versionen, etwa node:20-slim.

Erstellen wir ein Node.js-slim-Image auf Grundlage der aktuellen stabilen Debian-Version bookworm:

FROM node:bookworm-slim

Die Image-Größe ist bereits deutlich gesunken: von fast einem Gigabyte auf 231 MB. Auch ein Scan des Inhalts zeigt, dass der Software-Fußabdruck erheblich kleiner ist: 89=8 Abhängigkeiten und nur 37 Sicherheitslücken.

Das node:bookworm-slim-Image ist in puncto Container-Image-Größe und Sicherheitslage bereits ein besserer Ausgangspunkt.

Ein LTS-Node.js-Docker-Image

Bisher basierten unsere Node.js-Docker-Images auf der aktuellen Node.js-Version 22. Laut dem Node.js-Veröffentlichungsplan erreicht diese Version ihren offiziellen Status als Active LTS jedoch erst im Oktober 2024.

Was wäre, wenn wir für die von uns erstellten Node.js-Docker-Images immer auf Versionen mit Long-Term Support (LTS) setzen würden? Aktualisieren wir das Docker-Image-Tag entsprechend und erstellen ein neues Node.js-Image:

FROM node:lts-bookworm-slim

Die schlankere Node.js-LTS-Version (20.13.1) bringt eine ähnliche Anzahl an Abhängigkeiten und Sicherheitslücken mit sich. Das Image ist mit 219 MB etwas kleiner.

Wie sich herausstellt, wirkt sich die Wahl zwischen Node.js-Laufzeitversionen mit LTS und Current zwar je nach Ihren spezifischen Anforderungen aus, aber keine der beiden Optionen verändert den Software-Fußabdruck des Node.js-Images wesentlich.

Ist node:alpine die bessere Wahl für ein Node.js-Image?

Das Node.js-Docker-Team pflegt ein node:alpine-Image-Tag sowie Varianten davon, die bestimmte Versionen der Alpine-Linux-Distributionen mit den jeweiligen Node.js-Laufzeitversionen kombinieren.

Das Alpine-Linux-Projekt wird häufig für seine äußerst geringe Image-Größe gelobt. Das ist vorteilhaft, da es einen kleineren Software-Fußabdruck und entsprechend eine kleinere Angriffsfläche für Sicherheitslücken bedeutet.

FROM node:alpine
...

Das Docker-Image ist 167 MB groß und damit 64 MB kleiner als die Node.js-slim-Images. Im Alpine-Image-Tag wurden zum Zeitpunkt der Veröffentlichung nur 17 Betriebssystem-Abhängigkeiten und eine Sicherheitslücke gefunden. Das könnte darauf hindeuten, dass sich das alpine-Image-Tag sowohl hinsichtlich der Image-Größe als auch der Anzahl an Sicherheitslücken gut eignet.

Ist node:alpine die bessere Wahl für ein Node.js-Docker-Image?

Die Alpine-Variante des Node.js-Images kann eine insgesamt kleinere Image-Größe und sogar weniger Sicherheitslücken bieten. Dabei ist jedoch wichtig zu wissen, dass das Alpine-Projekt musl als Implementierung der C-Standardbibliothek verwendet. Debian-Node.js-Image-Tags wie bullseye oder slim setzen hingegen auf die Implementierung glibc. Diese Unterschiede können zu Leistungsproblemen, Funktionsfehlern oder potenziellen Anwendungsabstürzen führen. Itamar Turner-Trauring hat ebenfalls über unerwartete Laufzeitprobleme mit Alpine-Image-Tags für Python-Docker-Images geschrieben.

Wenn Sie ein Node.js-alpine-Image-Tag wählen, entscheiden Sie sich faktisch für eine inoffizielle Node.js-Laufzeitumgebung. Das Node.js-Docker-Team unterstützt Container-Image-Builds auf Alpine nicht offiziell. Daher bezeichnet es Alpine-basierte Image-Tags als experimentell und möglicherweise nicht einheitlich. Sie stellt sie über die folgenden inoffiziellen Builds bereit. Hier ein Zitat aus dem Repository der Unofficial Builds-Image-Tags:

Unofficial-builds versucht, grundlegende Node.js-Binärdateien für einige Plattformen bereitzustellen, die von Node.js entweder gar nicht oder nur teilweise unterstützt werden. Dieses Projekt bietet keinerlei Garantien, und seine Ergebnisse werden nicht rigoros getestet. Für Builds auf nodejs.org gelten sehr hohe Standards hinsichtlich Codequalität, Unterstützung auf den relevanten Plattformen sowie Zeitpunkt und Art der Bereitstellung. Die von unofficial-builds bereitgestellten Builds werden kaum oder gar nicht getestet; die Plattformen sind möglicherweise nicht in die offizielle Node.js-Testinfrastruktur eingebunden. Diese Builds werden der Entwickler-Community zur Verfügung gestellt, die Community soll jedoch bei ihrer Wartung mithelfen.

Einige wichtige Beobachtungen zur Kompatibilität des Node.js-alpine-Image-Tags:

  • Yarn ist inkompatibel (Issue #1716).

  • Wenn Sie node-gyp für die Cross-Kompilierung nativer C-Bindings benötigen, ist Python – eine Abhängigkeit dieses Prozesses – im Alpine-Image nicht enthalten. Sie müssen selbst eine Lösung finden (Issue #1706).

Distroless-Docker-Images für Node.js

Die letzten Vergleichskandidaten unseres Benchmarks sind Distroless-Images. Hier gibt es zwei Hauptoptionen: die ursprünglichen Distroless-Container-Images von Google und die neueren Distroless-Container-Images von Chainguard.

Was ist ein Distroless-Docker-Image?

Diese Images sind sogar schlanker als das Node.js-slim -Image-Tag, da sie ausschließlich auf die Anwendung und ihre Laufzeitabhängigkeiten ausgerichtet sind. Ein Distroless-Docker-Image enthält daher weder Paketmanager noch Shell oder andere allgemeine Tools als Abhängigkeiten. Dadurch sind Image-Größe und Angriffsfläche für Sicherheitslücken gering.

Google-Distroless-Image

Das Google-Distroless-Projekt pflegt ein laufzeitspezifisches Distroless-Docker-Image für Node.js. Es trägt den vollständigen Namen gcr.io/distroless/nodejs22-debian12 und ist in Googles Container-Registry verfügbar (der Teil gcr.io).

Hinweis: Es gibt auch ältere Kombinationen aus Debian und Node.js. Achten Sie jedoch darauf, eine aktiv gepflegte Version zu wählen. Die neuesten unterstützten Images finden Sie in der Tabelle im Distroless-GitHub-Repository.

Da die Distroless-Container-Images keine Software enthalten, können wir einen Docker-Multistage-Workflow nutzen, um Abhängigkeiten für unseren Container zu installieren und in die Distroless-Images zu kopieren:

FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY . /app
RUN npm install

FROM gcr.io/distroless/nodejs22-debian12
COPY --from=build /app /usr/src/app
WORKDIR /usr/src/app
CMD ["server.js"]

Beim Erstellen dieses Distroless-Docker-Images entsteht eine 177 MB große Datei. Damit ist sie kleiner als die Images mit den Tags slim und alpine .

Wenn Sie Distroless-Docker-Images verwenden möchten, sollten Sie einige wichtige Punkte beachten:

  • Sie basieren auf aktuellen stabilen Debian-Versionen. Das bedeutet, sie sind auf dem neuesten Stand und ihr Support-Ende liegt noch in weiter Ferne – ein großer Vorteil.

  • Da sie auf Debian basieren, verwenden sie die glibc-Implementierung und verursachen daher in der Produktion seltener unerwartete Probleme.

  • Sie werden schnell feststellen, dass das Distroless-Team keine Node.js-Runtime-Versionen mit genauer Versionsangabe pflegt. Daher müssen Sie auf den universellen Tag latest zurückgreifen, der regelmäßig aktualisiert wird, oder das Image anhand seines SHA256-Hashes zu einem bestimmten Zeitpunkt installieren.

Chainguard-Distroless-Image

Eine weitere Möglichkeit für Distroless-Images ist Chainguard. Chainguard bietet verschiedene Images an, darunter auch Images für ältere Node-Versionen und FIPS-fähige Versionen. Im kostenlosen Developer-Tarif stehen zwei Images zur Verfügung: „latest“, das zum Zeitpunkt der Erstellung dieses Artikels Node 22.1.0 ausführt, und „latest-dev“. Letzteres ergänzt das neueste Image um einen Paketmanager und eine Shell für Entwicklungs- und Debugging-Zwecke. 

Wir können ein ganz ähnliches Beispiel wie beim vorherigen Google-Distroless-Image verwenden:

FROM cgr.dev/chainguard/node:latest-dev AS build
WORKDIR /app
COPY . /app
USER root
RUN npm install

FROM cgr.dev/chainguard/node:latest
COPY --from=build /app /usr/src/app
WORKDIR /usr/src/app
CMD ["server.js"]

Das ergibt ein 142 MB großes Image, ähnlich dem Google-Distroless-Image. Es wurden keine Sicherheitslücken gemeldet. 

Erwähnenswert ist, dass alle Chainguard-Images auf Chainguards Wolfi-Linux-Distribution basieren. Wolfi wird gegen glibc kompiliert, daher gibt es keine Probleme mit der musl-Kompatibilität. 

Vergleich der Node.js-Docker-Image-Tags

Die folgende Tabelle fasst unseren Vergleich der verschiedenen Node.js-Docker-Image-Tags mit Stand vom 15. Mai 2024 zusammen, dem Datum der letzten Aktualisierung:

Image-Tag

Node.js-Runtime-Version

Betriebssystemabhängigkeiten

Sicherheitslücken im Betriebssystem

Schwerwiegende und kritische Sicherheitslücken

Mittlere Sicherheitslücken

Geringfügige Sicherheitslücken

Sicherheitslücken in der Node.js-Runtime

Image-Größe

Yarn verfügbar

node:latest

22.1.0

413

179

3

0

176

0

1135MB

Ja

node:bookworm

22.1.0

413

179

3

0

176

0

1135MB

Ja

node:bookworm-slim

22.1.0

88

37

2

0

35

0

233MB

Ja

node:lts-bookworm-slim

20.13.1

88

37

2

0

35

0

219MB

Ja

node:alpine

22.1.0

17

1

0

0

1

0

145MB

Ja

22.1.0

8

16

0

0

16

0

186MB

Nein

cgr.dev/chainguard/node:latest

22.1.0

25

0

0

0

0

0

134MB

Nein

cgr.dev/chainguard/node:latest-dev

22.1.0

66

0

0

0

0

0

651MB

Ja

Sehen wir uns die Daten und Erkenntnisse zu den verschiedenen Node.js-Image-Tags an und entscheiden wir, welches am besten geeignet ist.

Entwicklungsparität

Wenn Sie sich bei der Wahl des Node.js-Image-Tags an der Konsistenz zwischen Entwicklungs- und Produktionsumgebung orientieren – also in beiden Umgebungen exakt dieselbe Umgebung anstreben –, könnte dieser Kampf bereits verloren sein. In den meisten Fällen verwenden alle drei großen Betriebssysteme unterschiedliche C-Bibliotheken: Linux nutzt glibc, Alpine musl und macOS seine eigene BSD-libc-Implementierung.

Docker-Image-Größe

Manchmal kommt es auf die Größe an. Genauer gesagt geht es jedoch nicht darum, das kleinstmögliche Image zu haben, sondern den Software-Footprint insgesamt zu minimieren. In diesem Fall unterscheiden sich die Image-Tags slim in ihrer Größe kaum von ihren alpine-Pendants; alle liegen bei durchschnittlich etwa 211 MB pro Container-Image. Der Software-Footprint von slim-Images ist mit 89 jedoch weiterhin recht hoch (im Vergleich zu 17 bei alpine). Dadurch ist auch die Angriffsfläche für Sicherheitslücken größer: 28 bei slim gegenüber 0 bei alpine.

Sicherheitslücken

Sicherheitslücken sind ein wichtiges Thema und stehen im Mittelpunkt vieler Artikel, die erklären, warum Sie die Größe Ihrer Container-Images reduzieren sollten. Dabei kommt es jedoch stark auf die Art der Sicherheitsprobleme an.

Wenn wir die Images node und node:bullseye aufgrund ihres größeren Software-Footprints und der höheren Zahl an Sicherheitslücken außer Acht lassen, können wir uns auf eine kleinere Auswahl an Image-Typen konzentrieren. Beim Vergleich von slim, alpine und distroless fällt der Unterschied bei den schwerwiegenden und kritischen Sicherheitslücken absolut betrachtet gering aus: Die Anzahl liegt zwischen 0 und 2. Das ist ein beherrschbares Risiko, das für Ihren Anwendungsfall möglicherweise keine Rolle spielt.

Support und Zuverlässigkeit

Es ist von großem Vorteil, wenn das Node.js-Docker-Team Probleme bei der Erstellung Ihrer Container-Images priorisieren und angehen kann und diese zeitnah behoben werden. Bei allen Image-Tags, die nicht offiziell und Debian-basiert sind, können Sie diesen Punkt im Grunde nicht von Ihrer Checkliste abhaken.

Wenn Sie die Image-Tags node oder node:22.1.0-bookworm-slim verwenden, erhalten Sie unabhängig davon, ob Sie sich für ein vollständiges Betriebssystem-Image oder eine schlankere Version mit weniger Abhängigkeiten entscheiden, weiterhin die neueste Version der Node.js-Runtime. Obwohl es sich um eine gerade Version handelt (Node.js 22.1.0), war sie zum Zeitpunkt der Erstellung dieses Artikels noch nicht in den Long-Term-Support-Zyklus aufgenommen worden. Das bedeutet, dass neue Versionen anderer abhängiger Komponenten mitgeliefert werden, etwa die neuesten Versionen von npm, die bekanntermaßen fehlerhaftes neues Verhalten zeigen können und Zeit zur Stabilisierung benötigen.

Das Fazit?

Das ideale Node.js-Docker-Image ist eine schlanke Version des Betriebssystems, die auf einem modernen Debian-Betriebssystem basiert und eine stabile Node.js-Version mit aktivem Long-Term-Support verwendet.

Das bedeutet, dass Sie sich für den Node.js-Image-Tag node:lts-bookworm-slim entscheiden sollten. Ich bevorzuge deterministische Image-Tags. Deshalb würde ich eine kleine Änderung vornehmen und anstelle des Alias lts die tatsächliche Versionsnummer verwenden.

Der ideale Node.js-Docker-Image-Tag ist node:20.13.1-bookworm-slim.

Wenn Sie in einem erfahrenen DevOps-Team arbeiten, das benutzerdefinierte Basis-Images unterstützen kann, wäre Googles distroless-Image-Tag meine zweitbeste Empfehlung, da er die glibc-Kompatibilität für offizielle Node.js-Runtime-Versionen gewährleistet. Dieser Workflow erfordert allerdings Pflege. Ich empfehle ihn daher nur, wenn Sie die nötigen Ressourcen dafür haben.

Container-Sicherheit für DevSecOps

Finden und beheben Sie Container-Schwachstellen kostenlos mit Snyk.