10 Best Practices zur Containerisierung von Node.js-Anwendungen mit Docker
15. September 2022
0 Min. LesezeitHinweis der Redaktion:
14. September 2022: Sehen Sie sich unser neues, überarbeitetes Cheatsheet für die Containerisierung von Node.js-Webanwendungen mit Docker an.
Sie interessieren sich für Best Practices beim Aufbau von Node.js-Docker-Images für Ihre Webanwendungen? Dann sind Sie hier genau richtig!
Im folgenden Artikel geht es um Richtlinien für optimierte, sichere Node.js-Docker-Images für Prod-Umgebungen. Diese Tipps lassen sich auf jede Node.js-Anwendung übertragen, die Sie entwickeln möchten, und sind besonders hilfreich, wenn Sie:
eine Frontend-Anwendung mit SSR-Node.js-Funktionen für React (Server-Side Rendering) programmieren oder
wissen wollen, wie Sie ein Node.js-Docker-Image für Microservices auf Fastify, NestJS oder anderen Application-Frameworks erstellen.
Welchem Zweck dient dieser Leitfaden zur Containerisierung von node.js-Webanwendungen mit Docker?
Bei den meisten uns bekannten Blog-Beiträgen geht es um die simplen Grundlagen, wie Sie ein Node.js-Docker-Image zum Ausführen einer Anwendung anlegen. Oft kommt dabei jedoch die Sicherheit zu kurz und es fehlen durchdachte Best Practices für den Aufbau von Node.js-Docker-Images.
In diesem Beitrag erklären wir Schritt für Schritt, wie Sie Container für Node.js-Webanwendungen erstellen. Dafür zeigen wir an einer simplen, funktionierenden Dockerfile, worauf Sie bei jeder Dockerfile-Anweisung achten müssen, was schiefgehen kann und wie Sie solche Probleme beheben. Cheatsheet hier herunterladen.
Ein simples Node.js-Docker-Image
Die meisten Blogbeiträge, die wir kennen, beginnen und enden mit grundlegenden Dockerfile-Anweisungen zum Aufbau von Node.js-Docker-Images:
FROM node
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
CMD "npm" "start"
Legen wir los: Kopieren Sie das in eine Datei namens Dockerfile
, erstellen Sie einen Build und führen Sie ihn aus.
$ docker build . -t nodejs-tutorial
$ docker run -p 3000:3000 nodejs-tutorial
Das ist einfach und funktioniert.
Es gibt nur ein Problem: Diese Methode steckt voller Fehler und schlechter Praktiken für den Aufbau von Node.js-Docker-Images. Sie sollten auf keinen Fall so wie im obigen Beispiel vorgehen.
Beginnen wir mit der Verbesserung dieser Dockerfile zur Optimierung von Node.js-Webanwendungen mit Docker.
Sie können dieses Tutorial selbst durcharbeiten, indem Sie dieses Repository klonen.
Befolgen Sie diese zehn Tipps, um Node.js-Webanwendungen mit Docker zu optimieren:
Tags für explizite und deterministische Docker-Base-Images verwenden
Nur produktionsrelevante Abhängigkeiten im Node.js-Docker-Image installieren
Produktionsoptimierung von Node.js-Tools
Container nicht als Root ausführen
Node.js-Docker-Anwendungen sicher beenden
Node.js-Webanwendungen ordnungsgemäß herunterfahren
Sicherheitslücken im Node.js-Docker-Image aufspüren und schließen
Mit mehrstufigen Builds arbeiten
Unnötige Dateien in Node.js-Docker-Images vermeiden
Secrets im Docker-Build-Image mounten
1. Tags für explizite und deterministische Docker-Base-Images verwenden
Der Aufbau eigener Images auf Grundlage des node
-Docker-Images erscheint oft als einfachste Methode. Die Frage ist nur: Was wird dabei alles abgerufen und in das Image eingebunden? Docker-Images werden immer mit Tags referenziert. Geben Sie kein Tag an, wird standardmäßig das Tag :latest
verwendet.
Wenn Sie also Folgendes in Ihrer Dockerfile angeben, basiert Ihr Docker-Image immer auf der neuesten Image-Version der Node.js Docker Working Group:
FROM node
Der Aufbau von Images, die auf dem Standard-Image node
basieren, hat jedoch einige Nachteile:
Die Docker-Image-Builds sind inkonsistent. Genau wie bei der Verwendung von
lockfiles
für ein deterministischesnpm install
-Verhalten bei jeder Installation von npm-Paketen möchten wir auch deterministische Docker-Image-Builds erstellen. Verwenden wir das Image vom Node – also mit dem Tagnode:latest
–, wird jeder Build ein neu erstelltes Docker-Image abrufen, das aufnode
basiert. Mit dieser Art von deterministischem Verhalten wollen wir erst gar nicht anfangen.Das node-Docker-Image basiert auf einem vollständigen Betriebssystem mit Bibliotheken und Tools, von denen Sie noch nicht wissen, ob Sie sie zum Ausführen Ihrer Node.js-Webanwendung überhaupt brauchen. Das hat zwei Nachteile: Erstens bedeutet ein größeres Image eine größere Download-Datei. Das erhöht nicht nur die Speicheranforderungen, sondern auch die Download- und Re-Build-Zeiten für ein Image. Zweitens schleusen Sie damit womöglich Sicherheitslücken in Ihr Image ein, die durch Sicherheitslücken in all diesen Bibliotheken und Tools verursacht werden.
Das node
Docker-Image ist relativ groß und beinhaltet Hunderte von Sicherheitslücken unterschiedlichster Art und Gefährlichkeit – Stichwort „Schweregrad“. Mit diesem Image als Standard-Ausgangsbasis laden Sie also bei jedem Pull und Build mindestens 642 Sicherheitslücken und Hunderte Megabytes Image-Daten herunter.
Für bessere Docker-Image-Builds empfehlen wir daher Folgendes:
Verwenden Sie kleine Docker-Images. Damit verringern Sie den Software-Anteil auf dem Docker-Image, wodurch potenzielle Angriffsvektoren reduziert und der Image-Build-Prozess beschleunigt wird.
Nutzen Sie den Docker-Image-Digest, also den statischen SHA256-Hash des Images. Damit sorgen Sie dafür, dass Sie deterministische Docker-Image-Builds vom Base-Image erhalten.
Wir haben auch einen umfassenden Artikel darüber, wie Sie Ihr optimales Node.js-Docker-Image auswählen. In diesem Beitrag werden die genauen Gründe erklärt, warum eine aktuelle slim-Distribution von Debian mit einer langfristig unterstützten Node.js-Laufzeitversion ideal ist.
Das empfohlene Node.js-Docker-Image wäre:
FROM node:20.9.0-bullseye-slim
Dieses Tag für das Node.js-Docker-Image verweist auf eine bestimmte Node.js-Laufzeitversion (`16.17.`0) mit Langzeitunterstützung (Long-Term Support, LTS). Diese basiert auf der Image-Variante `bullseye` – der aktuellen offiziellen (stable) Debian-11-Version mit einem End-of-Life-Datum in ausreichender Ferne. Zudem handelt es sich um eine `slim`-Image-Variante mit einem minimalen Betriebssystem unter 200 MB (einschließlich Node.js-Laufzeitumgebung und Tools).
Eine der gängigen – und wenig reflektierten – Praktiken in Tutorials oder Leitfäden ist die Empfehlung der folgenden Docker-Anweisung für ein Base-Image:
FROM node:alpine
Damit legen Sie verwenden Sie das Docker-Image Node.js-Alpine als Ausgangsbasis. Die Frage ist nur, ob das wirklich ideal ist. Das Node.js-Alpine-Docker-Image wird oft wegen seines kleineren Software-Anteils empfohlen, hat jedoch einige Besonderheiten, die es nicht gerade ideal als Prod-Base-Image für Node.js-Application-Laufzeitumgebungen machen.
Node Alpine kurz erklärt
Node.js Alpine ist ein inoffizieller Docker-Container-Image-Build, der vom Node.js-Docker-Team gepflegt wird. Das Node.js-Image kommt mit dem Alpine-Betriebssystem, das mit den minimalen busybox-Softwaretools und der Bibliotheksimplementierung musl C arbeitet. Nicht zuletzt wegen dieser beiden Eigenschaften des Node.js-Alpine-Image wird das Docker-Image inoffiziell vom Node.js-Team unterstützt. Zudem können viele Sicherheitslücken-Scanner Software-Artefakte oder Laufzeitumgebungen auf Node.js-Alpine-Images nicht leicht erkennen, was beim Schutz Ihrer Container-Images kontraproduktiv ist.
Unabhängig vom verwendeten Image-Tag für Node.js-Alpine können Sie mit einem Word-Alias als Base-Image-Anweisung weiterhin neue Builds dieses Tags abrufen, da Docker-Image-Tags veränderbar sind. Die dafür notwendige Hashfunktion SHA256
erhalten wir mit dem Docker Hub für dieses Node.js-Tag oder durch Ausführen des folgenden Befehls, nachdem wir dieses Image lokal per Pull abgerufen und das Feld Digest
im Output ausfindig gemacht haben:
$ docker pull node:20.9.0-bullseye-slim
20.9.0-bullseye-slim: Pulling from library/node
ca426296fe92: Pull complete
0d5f60f923bb: Pull complete
cc6fa81c4559: Pull complete
ec5e8e3b63b3: Pull complete
ca7cb04b0758: Pull complete
Digest: sha256:330fa0342b6ad2cbdab30ac44195660af5a1f298cc499d8cbdf7496b02ea17d8
Status: Downloaded newer image for node:20.9.0-bullseye-slim
docker.io/library/node:20.9.0-bullseye-slim
Die Hashfunktion SHA256
lässt sich auch mit diesem Befehl finden:
$ docker images --digests
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
node 20.9.0-bullseye-slim sha256:330fa0342b6ad2cbdab30ac44195660af5a1f298cc499d8cbdf7496b02ea17d8 9ea15fe618bd 7 days ago 200MB
Jetzt können wir die Dockerfile für dieses Node.js-Docker-Image wie folgt aktualisieren:
FROM node@sha256:330fa0342b6ad2cbdab30ac44195660af5a1f298cc499d8cbdf7496b02ea17d8
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
CMD "npm" "start"
Die obige Dockerfile gibt allerdings nur den Namen des Node.js-Docker-Images ohne Image-Tag an. So bleibt unklar, welches Image-Tag genau verwendet wird. Und das wiederum beeinträchtigt Lesbarkeit, Wartung und Entwicklung.
Beheben wir nun dieses Problem mit einem Update der Dockerfile, damit diese das vollständige Base-Image-Tag für die Node.js-Version gemäß dem Hash SHA256
enthält:
FROM node:20.9.0-bullseye-slim@sha256:330fa0342b6ad2cbdab30ac44195660af5a1f298cc499d8cbdf7496b02ea17d8
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
CMD "npm" "start"
Mit dem Docker-Image-Digest erhalten Sie zwar sicher ein deterministisches Image, das jedoch für einige Image-Scanning-Tools verwirrend oder kontraproduktiv sein könnte, da diese nicht wissen, wie sie das Image interpretieren sollen. Aus diesem Grund empfehlen wir die Verwendung einer konkreten Node.js-Laufzeitversion wie `16.17.0`. Theoretisch kann diese zwar auch verändert oder überschrieben werden, in der Praxis wird aber bei Sicherheits- und anderen Updates eine neue Version (z. B. `16.17.1`) übernommen. Das reicht aus, damit wir bei dieser Version von deterministischen Builds ausgehen können.
Deshalb sollte unsere Dockerfile nun so aussehen:
FROM node:16.17.0-bullseye-slim
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
CMD "npm" "start"
Weitere Tipps und Best Practices für den Aufbau sicherer Container-Images
2. Nur produktionsrelevante Abhängigkeiten im Node.js-Docker-Image installieren
Die folgende Dockerfile-Anweisung installiert alle Abhängigkeiten im Container, einschließlich überflüssiger devDependencies
, die Sie für eine funktionierende Anwendung nicht brauchen. Dadurch entsteht ein vermeidbares Sicherheitsrisiko durch Pakete, die als Dev-Abhängigkeiten genutzt werden. Außerdem wird die Image-Größe unnötig aufgeblasen.
RUN npm install
Falls Sie meinen vorherigen Leitfaden zu den 10 Best-Practices für NPM-Sicherheit kennen, wollen Sie wahrscheinlich deterministische Builds mit npm ci
durchsetzen. Dadurch vermeiden Sie Überraschungen in einem kontinuierlichen Integrationsablauf, da ein solcher CI-Flow bei Abweichungen von der Lockfile angehalten wird.
Bei einem Docker-Image für die Produktion möchten wir sicherstellen, dass wir produktionsbezogene Abhängigkeiten ausschließlich deterministisch anlegen. Aus diesem Grund empfehlen wir folgende Best Practice zum Installieren von npm-Abhängigkeiten in einem Container-Image:
RUN npm ci --only=production
Unsere Dockerfile enthält jetzt also Folgendes:
FROM node:20.9.0-bullseye-slim
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm ci --only=production
CMD "npm" "start"
Mehr zum Thema erfahren Sie unseren Artikel über Softwareabhängigkeiten.
3. Produktionsoptimierung von Node.js-Tools
Bei einem Node.js-Docker-Image für die Produktion wollen Sie wahrscheinlich sicherstellen, dass alle Frameworks und Bibliotheken die optimalen Performance- und Security-Einstellungen haben.
Deshalb fügen Sie jetzt die folgende Dockerfile-Anweisung hinzu:
ENV NODE_ENV production
Vielleicht halten Sie das jetzt für überflüssig, weil wir uns ja in der npm install
-Stufe schon auf produktionsbezogene Abhängigkeiten beschränkt haben. Warum also ist dieser Schritt trotzdem notwendig?
Entwicklerinnen und Entwickler verbinden die Umgebungsvariablen-Einstellung NODE_ENV=production
häufig mit der Installation von produktionsbezogenen Abhängigkeiten. Diese Einstellung hat allerdings auch andere Auswirkungen, die wir beachten müssen.
Einige Frameworks und Bibliotheken aktivieren womöglich nur dann die optimierte, für die Produktion geeignete Konfiguration, wenn die Umgebungsvariable NODE_ENV
auf production
gesetzt ist. Abgesehen davon, ob wir das nun für eine gute oder schlechte Vorgehensweise für Frameworks halten, sollten Sie dies immer im Hinterkopf behalten.
Beispielsweise wird in der Dokumentation von Express darauf hingewiesen, wie wichtig die Festlegung dieser Umgebungsvariable für leistungs- und sicherheitsrelevante Optimierungen ist.
Die Auswirkungen der Variable NODE_ENV
auf die Leistung kann erheblich sein.
Dynatrace beschreibt in einem Blog-Beitrag ausführlich die drastischen Folgen, wenn die NODE_ENV in Express-Anwendungen weggelassen wird.
Viele andere notwendige Bibliotheken erfordern möglicherweise die Festlegung dieser Variable, weshalb wir das auch in unserer Dockerfile tun sollten.
Die aktualisierte Dockerfile mit der festgelegten Umgebungsvariable NODE_ENV
sollte nun so aussehen:
FROM node:20.9.0-bullseye-slim
ENV NODE_ENV production
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm ci --only=production
CMD "npm" "start"
4. Container nicht als Root ausführen
Das Prinzip der geringsten Rechte ist ein Sicherheitsgrundsatz aus den Anfängen von Unix, an den wir uns auch beim Ausführen unserer containerisierten Node.js-Webanwendungen halten sollten.
Die Bedrohungsbewertung ist recht simpel: Kann ein Angreifer die Webanwendung auf eine Weise kompromittieren, die eine Command-Injection oder Directory Path Traversal erlaubt, werden diese bei dem Benutzer aufgerufen, der als Owner für den Anwendungsprozess zuständig ist. Handelt es sich dabei um den Root-Prozess, kann dieser Benutzer praktisch alles mit dem Container-Inhalt anstellen, z. B. eine Container-Escape oder Privilege-Escalation versuchen. Warum sollte man das riskieren wollen?
Halten Sie sich IMMER an den Grundsatz: „Freunde lassen nicht zu, dass Freunde Container als Root ausführen!“
Beim offiziellen Docker-Image node
sowie Varianten wie alpine
gibt es immer einen Benutzer mit geringsten Rechten: den User node
. Es reicht jedoch nicht aus, den Prozess einfach als node
auszuführen. Folgendes ist z. B. für die Anwendungsfunktion nicht ideal:
USER node
CMD "npm" "start"
Der Grund dafür ist, dass die Dockerfile-Anweisung USER
lediglich dafür sorgt, dass der Prozess-Owner der Benutzer node
ist. Und was ist mit den ganzen Dateien, die wir zuvor mit der COPY
-Anweisung kopiert haben? Deren Owner ist weiterhin der Benutzer „root“. So funktioniert Docker mit den Standardeinstellungen.
Die vollständige korrekte Methode zum Löschen von Berechtigungen finden Sie im Folgenden (einschließlich der bereits für die Dockerfile vorgestellten Tipps):
FROM node:20.9.0-bullseye-slim
ENV NODE_ENV production
WORKDIR /usr/src/app
COPY --chown=node:node . /usr/src/app
RUN npm ci --only=production
USER node
CMD "npm" "start"
5. Node.js-Docker-Anwendungen sicher beenden
Einer der häufigsten Fehler, auf die ich immer wieder in Blogbeiträgen und Artikeln zum Ausführen von Node.js-Anwendungen in Docker-Containern stoße, ist die Art und Weise, wie der Prozess aufgerufen wird. Alle folgenden Einstellungen und deren Varianten sollten Sie unbedingt vermeiden:
CMD “npm” “start”
CMD [“yarn”, “start”]
CMD “node” “server.js”
CMD “start-app.sh”
Sehen wir uns das genauer an und lassen Sie mich kurz erklären, warum jeder dieser Aufrufprozesse nicht verwendet werden sollte.
Um den Kontext für das richtige Ausführen und Beenden von Node.js-Docker-Anwendungen zu verstehen, muss man sich folgender Probleme bewusst sein:
Eine Orchestrierungs-Engine wie Docker Swarm, Kubernetes oder sogar die Docker-Engine selbst müssen Signale an den Prozess im Container senden können. Meistens handelt es sich dabei um Signale wie
SIGTERM
oderSIGKILL
, um die Anwendung zu beenden.Wird der Prozess indirekt ausgeführt, ist nicht immer gewährleistet, dass er diese Signale empfängt.
Der Linux-Kernel behandelt Prozesse, die als Process ID 1 (PID) ausgeführt werden, anders als andere Prozess-IDs.
Sehen wir uns vor diesem Hintergrund einmal die Optionen zum Aufrufen des Container-Prozesses an, angefangen mit dem Beispiel aus der Dockerfile, die wir gerade erstellen:
CMD "npm" "start"
Hier gibt es eine doppelte Einschränkung: Erstens führen wir die Node-Anwendung direkt durch Aufrufen des npm-Clients aus. Woher wissen wir, ob die npm-CLI alle Events an die Node-Laufzeitumgebung weiterleitet? Tatsächlich ist das nicht der Fall, was sich einfach herausfinden lässt.
Legen Sie dafür in Ihrer Node.js-Anwendung einen Event-Handler für das SIGHUP
-Signal fest, das bei jedem Ihrer gesendeten Events in der Konsole protokolliert wird. Hier ein einfaches Code-Beispiel, wie das aussehen könnte:
function handle(signal) {
console.log(`*^!@4=> Received event: ${signal}`)
}
process.on('SIGHUP', handle)
Führen Sie dann den Container aus. Sobald er hochgefahren ist, senden Sie das SIGHUP
-Signal über die docker
-CLI mit dem Befehlszeilen-Flag --signal
:
$ docker kill --signal=SIGHUP elastic_archimedes
Nichts passiert. Das liegt daran, dass der npm-Client keine Signale an den Node-Prozess weiterleitet, den er erzeugt hat.
Die andere Einschränkung hat mit den verschiedenen Formen zu tun, wie Sie die CMD
-Anweisung in die Dockerfile schreiben können. Dafür gibt es zwei Optionen:
die Shellform-Notation, bei der der Container einen Shell-Interpreter für den Prozess erzeugt. In diesem Fall leitet die Shell die Signale womöglich nicht korrekt an Ihre Prozesse weiter.
die Execform-Notation, die einen Prozess erzeugt, ohne ihn in eine Shell zu verpacken. Diese wird mit der JSON-Array-Notation angegeben und sieht z. B. so aus:
CMD [“npm”, “start”]
. Alle an den Container gesendeten Signale werden direkt an den Prozess gesendet.
Da wir das nun wissen, verbessern jetzt wir unsere Dockerfile-Anweisung zur Prozessausführung wie folgt:
CMD ["node", "server.js"]
Wir rufen jetzt den Node-Prozess direkt auf und stellen sicher, dass er alle an ihn gesendeten Signale auch ohne Shell-Interpreter empfängt.
Dadurch entsteht jedoch ein weiteres Problem:
Werden Prozesse als PID 1 ausgeführt, übernehmen sie einige Aufgaben eines Init-Systems, das normalerweise für die Initialisierung eines Betriebssystems und der Prozesse zuständig ist. Der Kernel behandelt PID 1 anders als andere Prozess-Identifier. Diese Sonderbehandlung durch den Kernel bedeutet, dass die Verarbeitung eines SIGTERM
-Signals für einen laufenden Prozess kein standardmäßiges Fallback-Verhalten zum Beenden des Prozesses liefert, sofern der Prozess nicht bereits einen Handler dafür festgelegt hat.
Entsprechend lautet die Empfehlung der Node.js Docker Working Group: „Node.js wurde nicht für die Ausführung als PID 1 entwickelt, was beim Ausführen in Docker zu unerwartetem Verhalten führt. Beispielsweise reagiert ein als PID 1 ausgeführter Node.js-Prozess nicht auf SIGINT (CTRL-C) und ähnliche Signale.“
Das lässt sich mit einem Tool vermeiden, das sich wie ein Init-Prozess verhält: Es wird mit PID 1 aufgerufen, erzeugt unsere Node.js-Anwendung als weiteren Prozess und sorgt zugleich dafür, dass alle Signale zu diesem Node.js-Prozess weitergeleitet werden. Grundsätzlich sollten Tools möglichst abgespeckt sein (geringer Tools-Footprint), damit wir nicht weitere Sicherheitslücken in unserem Container-Image riskieren.
Bei Snyk verwenden wir dafür u. a. das Tool dumb-init, weil es statisch verlinkt ist und einen kleinen Footprint hat. So wird es eingerichtet:
RUN apt-get update && apt-get install -y --no-install-recommends dumb-init
CMD ["dumb-init", "node", "server.js"]
Das Ergebnis ist die folgende Dockerfile
. Wie Sie sehen, haben wir die dumb-init
-Paketinstallation direkt hinter der Image-Deklaration eingefügt, damit wir das Layer-Caching von Docker verwenden können:
FROM node:20.9.0-bullseye-slim
RUN RUN apt-get update && apt-get install -y --no-install-recommends dumb-init
ENV NODE_ENV production
WORKDIR /usr/src/app
COPY --chown=node:node . .
RUN npm ci --only=production
USER node
CMD ["dumb-init", "node", "server.js"]
Wenn wir die Docker-Anweisung RUN
wie bei RUN apt-get update && apt-get install
zum Hinzufügen von Software verwenden, hinterlassen wir einige Informationen über das Docker-Image. Um solche Informationen nach diesem Befehl zu bereinigen, können wir ihn folgendermaßen erweitern und so ein schlankeres Docker-Image erhalten:
FROM node:20.9.0-bullseye-slim
RUN apt-get update && apt-get install -y --no-install-recommends dumb-init
ENV NODE_ENV production
WORKDIR /usr/src/app
COPY --chown=node:node . .
RUN npm ci --only=production
USER node
CMD ["dumb-init", "node", "server.js"]
Tipp: Noch besser ist es, wenn Sie das dumb-init
-Tool in einem früheren Build-Stage-Image installieren und dann die resultierende /usr/bin/dumb-init
zum Schluss in das fertige Container-Image kopieren, um das Image möglichst sauber zu halten. Auf mehrstufige Docker-Builds gehen wir später noch in diesem Leitfaden ein.
Gut zu wissen: Die Befehle docker kill
und docker stop
senden nur Signale zum Container-Prozess mit der PID 1. Wenn Sie Ihre Node.js-Anwendung mit einem Shell-Skript ausführen, sollten Sie beachten, dass eine Shell-Instanz – wie z. B. /bin/sh
– keine Signale an Child-Prozesse weiterleitet. Das bedeutet, dass Ihre App niemals ein SIGTERM
erhält.
6. Node.js-Webanwendungen ordnungsgemäß herunterfahren
Apropos Prozess-Signale zum Beenden von Anwendungen: Wichtig ist, dass Anwendungen korrekt und ohne Probleme für Benutzer heruntergefahren werden.
Empfängt eine Node.js-Anwendung ein Interrupt-Signal (wie SIGINT
oder CTRL+C
), verursacht das einen abrupten Prozessabbruch – es sei denn, Sie haben Event-Handler für ein anderes Verhalten festgelegt. Das bedeutet, dass mit einer Webanwendung verbundene Clients sofort getrennt werden. Stellen Sie sich nun Hunderte von Kubernetes-orchestrierten Node.js-Webcontainer vor, die je nach Bedarf zur Skalierung oder für das Fehler-Management gestartet und beendet werden. Das wäre ein eher suboptimales Benutzererlebnis.
Dieses Problem lässt sich einfach simulieren. Nehmen wir z. B. eine Fastify-Webanwendung mit einer inhärenten verzögerten Antwort von 60 Sekunden für einen Endpunkt:
fastify.get('/delayed', async (request, reply) => {
const SECONDS_DELAY = 60000
await new Promise(resolve => {
setTimeout(() => resolve(), SECONDS_DELAY)
})
return { hello: 'delayed world' }
})
const start = async () => {
try {
await fastify.listen(PORT, HOST)
console.log(`*^!@4=> Process id: ${process.pid}`)
} catch (err) {
fastify.log.error(err)
process.exit(1)
}
}
start()
Führen Sie diese Anwendung aus und senden Sie nach dem Ausführen eine einfache HTTP-Anforderung an diesen Endpunkt:
$ time curl https://localhost:3000/delayed
Verwenden Sie im Konsolenfenster während der Node.js-Ausführung den Tastaturbefehl CTRL+C
, wird der Curl-Request abrupt beendet. Genau das würden Ihre Benutzer erleben, wenn Container einfach beendet werden.
Ein besseres Benutzererlebnis erreichen Sie so:
Legen Sie einen Event-Handler für die verschiedenen Terminierungssignale wie
SIGINT
undSIGTERM
fest.Der Handler wartet auf Clean-up-Operationen wie Datenbankverbindungen oder laufende HTTP-Requests.
Dann beendet der Handler den Node.js-Prozess.
Insbesondere bei Fastify können wir unseren Handler-Aufruf auf fastify.close() setzen, worauf uns ein Promise zurückgegeben wird, auf das wir warten werden. Fastify übernimmt auch die Reaktion auf jede neue Verbindung mit dem HTTP-Statuscode 503, um die Nichtverfügbarkeit der Anwendung zu signalisieren.
Fügen wir nun unseren Event-Handler hinzu:
async function closeGracefully(signal) {
console.log(`*^!@4=> Received signal to terminate: ${signal}`)
await fastify.close()
// await db.close() if we have a db connection in this app
// await other things we should cleanup nicely
process.kill(process.pid, signal);
}
process.once('SIGINT', closeGracefully)
process.once('SIGTERM', closeGracefully)
Auch wenn dies zugegebenermaßen eher ein allgemeines Problem für Webanwendungen als für Dockerfiles darstellt, ist dies besonders für orchestrierte Umgebungen wichtig.
7. Sicherheitslücken im Node.js-Docker-Image aufspüren und schließen
Erinnern Sie sich noch, dass wir erwähnt haben, wie wichtig kleine Docker-Base-Images für unsere Node.js-Anwendungen sind? Setzen wir das nun in die Praxis um.
Ich werde jetzt unser Docker-Image mit der Snyk CLI testen. Sie können sich hier für ein kostenloses Snyk-Konto registrieren.
$ npm install -g snyk
$ snyk auth
$ snyk container test node:20.9.0-bullseye-slim --file=Dockerfile
Der erste Befehl installiert die Snyk-CLI, gefolgt von einer schnellen Anmeldung über die Befehlszeile, um einen API-Schlüssel abzurufen. Dann können wir den Container auf Sicherheitsprobleme überprüfen. Das Ergebnis:
Organization: lirantal
Package manager: deb
Target file: Dockerfile
Project name: docker-image|node
Docker image: node:20.9.0-bullseye-slim
Platform: linux/arm64
Base image: node:lts-bullseye-slim
Licenses: enabled
Tested 97 dependencies for known issues, found 44 issues.
According to our scan, you are currently using the most secure version of the selected base image
Snyk hat 97 Betriebssystem-Abhängigkeiten erkannt, einschließlich unserer ausführbaren Node.js-Laufzeitdatei, fand aber keine anfälligen Laufzeitversionen. Allerdings gibt es 44 Sicherheitslücken bei der Software im Container-Image. 43 dieser Abhängigkeiten sind Schwachstellen mit geringem Schweregrad. Aber es gibt eine Sicherheitslücke mit kritischen Schwergrad in einer zlib-Bibliothek:
✗ Low severity vulnerability found in apt/libapt-pkg6.0
Description: Improper Verification of Cryptographic Signature
Info: https://snyk.io/vuln/SNYK-DEBIAN11-APT-522585
Introduced through: apt/libapt-pkg6.0@2.2.4, apt@2.2.4
From: apt/libapt-pkg6.0@2.2.4
From: apt@2.2.4 > apt/libapt-pkg6.0@2.2.4
From: apt@2.2.4
Image layer: Introduced by your base image (node:lts-bullseye-slim)
✗ Critical severity vulnerability found in zlib/zlib1g
Description: Out-of-bounds Write
Info: https://snyk.io/vuln/SNYK-DEBIAN11-ZLIB-2976151
Introduced through: meta-common-packages@meta
From: meta-common-packages@meta > zlib/zlib1g@1:1.2.11.dfsg-2+deb11u1
Image layer: Introduced by your base image (node:lts-bullseye-slim)
Fixed in: 1:1.2.11.dfsg-2+deb11u2
Beheben von Schwachstellen in Docker-Images
Ein schneller und effektiver Weg zu sicherer Software im Docker-Image ist der Neuaufbau des Docker-Images. Dafür brauchen Sie das zuvor erstellte Docker-Base-Image, das diese Updates für Sie abruft. Eine andere Möglichkeit ist die Installation ausgewählter Betriebssystem-Updates für Pakete, einschließlich Sicherheitsfixes.
Beim offiziellen Node.js-Docker-Image braucht es vielleicht länger, bis Image-Updates verfügbar sind. Dann wäre der Rebuild des Node.js-Docker-Images 16.17.0-bullseye-slim
oder lts-bullseye-slim
keine effektive Lösung. Alternativ können Sie auch Ihr eigenes Base-Image mit aktueller Software von Debian pflegen. In unserer Dockerfile
setzen wir das so um:
RUN apt-get update && apt-get upgrade -y
Nach dem Aufbau des Node.js-Docker-Images führen wir den Snyk-Security-Scan mit der neu hinzugefügten RUN
-Anweisung aus:
✗ Low severity vulnerability found in apt/libapt-pkg6.0
Description: Improper Verification of Cryptographic Signature
Info: https://snyk.io/vuln/SNYK-DEBIAN11-APT-522585
Introduced through: apt/libapt-pkg6.0@2.2.4, apt@2.2.4
From: apt/libapt-pkg6.0@2.2.4
From: apt@2.2.4 > apt/libapt-pkg6.0@2.2.4
From: apt@2.2.4
Image layer: Introduced by your base image (node:20.9.0-bullseye-slim)
…
Tested 98 dependencies for known issues, found 43 issues.
According to our scan, you are currently using the most secure version of the selected base image
Dies führte dazu, dass eine weitere Betriebssystemabhängigkeit hinzugefügt wurde (98 gegenüber 97 zuvor). Allerdings sind jetzt alle 43 Sicherheitslücken, die sich auf dieses Node.js-Docker-Image auswirken, von geringem Schweregrad. Außerdem haben wir die kritische zlib-Sicherheitslücke geschlossen. Das ist ein ausgezeichnetes Ergebnis!
Was wäre passiert, hätten wir die Base-Image-Anweisung FROM node
verwendet? Oder noch besser: Nehmen wir an, Sie hätten auf ein bestimmtes Node.js-Docker-Base-Image wie dieses verwiesen:
FROM node:14.2.0-slim
…
✗ High severity vulnerability found in node
Description: Memory Corruption
Info: https://snyk.io/vuln/SNYK-UPSTREAM-NODE-570870
Introduced through: node@14.2.0
From: node@14.2.0
Introduced by your base image (node:14.2.0-slim)
Fixed in: 14.4.0
✗ High severity vulnerability found in node
Description: Denial of Service (DoS)
Info: https://snyk.io/vuln/SNYK-UPSTREAM-NODE-674659
Introduced through: node@14.2.0
From: node@14.2.0
Introduced by your base image (node:14.2.0-slim)
Fixed in: 14.11.0
Organization: snyk-demo-567
Package manager: deb
Target file: Dockerfile
Project name: docker-image|node
Docker image: node:14.2.0-slim
Platform: linux/amd64
Base image: node:14.2.0-slim
Tested 78 dependencies for known issues, found 82 issues.
Base Image Vulnerabilities Severity
node:14.2.0-slim 82 23 high, 11 medium, 48 low
Recommendations for base image upgrade:
Minor upgrades
Base Image Vulnerabilities Severity
node:14.15.1-slim 71 17 high, 7 medium, 47 low
Major upgrades
Base Image Vulnerabilities Severity
node:15.4.0-slim 71 17 high, 7 medium, 47 low
Alternative image types
Base Image Vulnerabilities Severity
node:14.15.1-buster-slim 55 12 high, 4 medium, 39 low
node:14.15.3-stretch-slim 71 17 high, 7 medium, 47 low
Auch wenn eine spezielle Node.js-Laufzeitversion wie FROM node:14.2.0-slim
auszureichen scheint, weil Sie eine bestimmte Version (`14.2.0`) angegeben und außerdem ein kleines Container-Image (mit dem `slim`-Image-Tag) verwendet haben, kann Snyk zwei Sicherheitslücken in zwei primären Quellen finden:
In der Node.js-Laufzeitumgebung selbst – haben Sie oben in dem Bericht die beiden gravierendsten Sicherheitslücken gesehen? Dabei handelt es sich um allgemein bekannte Sicherheitsprobleme der Node.js-Laufzeitumgebung. Diese lassen sich mit einem sofortigen Upgrade auf eine neuere Node.js-Version beheben. Snyk gibt aber nicht nur diesen Hinweis, sondern zeigt Ihnen auch, welche Version Sie für den Fix brauchen: 14.11.0, wie Sie in den Ergebnissen sehen können.
Auf diesem Debian-Base-Image installierte Tools und Bibliotheken, wie glibc, bzip2, gcc, perl, bash, tar oder libcrypt. Auch wenn diese anfälligen Versionen in dem Container keine unmittelbare Bedrohung darstellen, warum sollten wir sie behalten, wenn wir sie nicht brauchen?
Und das Beste an diesem Snyk-CLI-Report? Snyk empfiehlt Ihnen andere Base-Images, auf die Sie ausweichen können. Sie müssen das also nicht selbst herausfinden. Die Suche nach alternativen Images kann viel Zeit in Anspruch nehmen und Snyk erspart Ihnen diesen Aufwand.
Mein Tipp:
Sollten Sie Docker-Images in einer Registry wie Docker Hub oder Artifactory verwalten, können Sie sie einfach in Snyk importieren, damit die Plattform diese Schwachstellen für Sie findet. Dadurch profitieren Sie auch von Empfehlungen in der Snyk-Benutzeroberfläche und einem ständigen Monitoring Ihrer Docker-Images auf neu entdeckte Sicherheitslücken.
Verwenden Sie die Snyk-CLI in Ihrer CI-Automatisierung. Die CLI ist sehr flexibel und genau dafür haben wir sie entwickelt – damit Sie sie für all Ihre benutzerdefinierten Workflows anwenden können. Wir bieten auch Snyk für GitHub Actions an, falls Ihnen das lieber ist.
Weitere Methoden für das Schwachstellen-Management in Container-Images finden Sie in unserem Leitfaden zur Container-Sicherheit.
8. Mit mehrstufigen Builds arbeiten
Mehrstufige Builds sind ideal, wenn Sie statt einer simplen, aber womöglich fehlerhaften Dockerfile Ihr Docker-Image in einzelnen Schritten aufbauen wollen, um keine vertraulichen Informationen preiszugeben. Das bietet auch weitere Vorteile. Zum Beispiel können wir ein größeres Docker-Base-Image verwenden, um unsere Abhängigkeiten zu installieren und beliebige native npm-Pakete zu kompilieren – und dann kopieren wir all diese Artefakte in ein kleines Prod-Base-Image (wie in unserem Alpine-Beispiel).
Vertrauliche Informationen schützen
Dass keine vertraulichen Daten preisgegeben werden, ist ein häufigerer Anwendungsfall, als man vermuten mag.
Bei Docker-Images für geschäftliche Zwecke ist die Wahrscheinlichkeit hoch, dass auch private npm-Pakete gepflegt werden müssen. Also müssen Sie wahrscheinlich eine Möglichkeit finden, um diesen geheimen NPM_TOKEN
für die npm-Installation verfügbar zu machen.
Hier ein Beispiel:
FROM node:20.9.0-bullseye-slim
RUN apt-get update && apt-get install -y --no-install-recommends dumb-init
ENV NODE_ENV production
ENV NPM_TOKEN 1234
WORKDIR /usr/src/app
COPY --chown=node:node . .
#RUN npm ci --only=production
RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc && \
npm ci --only=production
USER node
CMD ["dumb-init", "node", "server.js"]
Auf diese Weise bleibt die .npmrc
-Datei beim geheimen npm-Token im Docker-Image. Eine mögliche Verbesserung wäre, sie nachher zu löschen, z. B. so:
RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc && \
npm ci --only=production
RUN rm -rf .npmrc
Jetzt ist die .npmrc
-Datei jedoch auf einem anderen Layer des Docker-Images verfügbar. Wird dieses Docker-Image öffentlich bereitgestellt oder jemand kann irgendwie darauf zugreifen, ist Ihr Token kompromittiert. Besser wäre Folgendes:
RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc && \
npm ci --only=production; \
rm -rf .npmrc
Jetzt haben wir das Problem, dass die Dockerfile selbst als Secret Asset behandelt werden muss, weil sie das geheime npm-Token enthält.
Docker bietet zum Glück die praktische Möglichkeit, Argumente an den Build-Prozess zu übergeben.
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc && \
npm ci --only=production; \
rm -rf .npmrc
Dann bauen wir den Build wie folgt auf:
$ docker build . -t nodejs-tutorial --build-arg NPM_TOKEN=1234
Wahrscheinlich haben Sie gedacht, dass wir jetzt fertig sind. Aber da muss ich Sie leider enttäuschen.
So ist das nun mal beim Thema Sicherheit: Einige offensichtliche Dinge stellen sich manchmal als problematisch heraus.
Sie fragen sich jetzt, was das Problem sei? Build-Argumente, die auf diese Weise an Docker übergeben werden, werden im History Log gespeichert. Sehen wir uns das einmal genauer an. Führen Sie bitte diesen Befehl aus:
$ docker history nodejs-tutorial
Darauf wird Folgendes ausgegeben:
IMAGE CREATED CREATED BY SIZE COMMENT
b4c2c78acaba About a minute ago CMD ["dumb-init" "node" "server.js"] 0B buildkit.dockerfile.v0
<missing> About a minute ago USER node 0B buildkit.dockerfile.v0
<missing> About a minute ago RUN |1 NPM_TOKEN=1234 /bin/sh -c echo "//reg… 5.71MB buildkit.dockerfile.v0
<missing> About a minute ago ARG NPM_TOKEN 0B buildkit.dockerfile.v0
<missing> About a minute ago COPY . . # buildkit 15.3kB buildkit.dockerfile.v0
<missing> About a minute ago WORKDIR /usr/src/app 0B buildkit.dockerfile.v0
<missing> About a minute ago ENV NODE_ENV=production 0B buildkit.dockerfile.v0
Haben Sie das geheime npm-Token entdeckt? Genau das meine ich.
Das ist eine tolle Möglichkeit, um Secrets für das Container-Image zu verwalten. Aber noch besser sind mehrstufige Builds, damit dieses Problem erst gar nicht entsteht, wie ich gleich erklären werde. Auch möchte ich Ihnen zeigen, wie Sie minimale Images anlegen.
Mehrstufige Builds für node.js-Docker-Images kurz erklärt
Für den Aufbau unserer Node.js-Docker-Images können wir einen bekannten Grundsatz aus der Softwareentwicklung anwenden: „Separations of Concerns“, also die Trennung der Belange. Wir verwenden ein Image, um alles zu erstellen, was wir zum Ausführen der Node.js-Anwendung brauchen. Für eine Node.js-Umgebung bedeutet das die Installation von npm-Paketen und ggf. die Kompilierung nativer npm-Module. Das ist unsere erste Stufe.
Das zweite Docker-Image, das die zweite Stufe des Docker-Builds darstellt, wird das Docker-Prod-Image sein. Diese zweite und letzte Stufe ist das Image, das wir de facto optimieren und zum Veröffentlichen an die Registry geben (sofern wir eine haben). Das erste Image (unser build
-Image) wird verworfen und bleibt bis zur Bereinigung als Image in dem Docker-Host erhalten, der es erstellt hat.
Hier nun das Update unserer Dockerfile, das unseren bisherigen Fortschritt zeigt, aber in zwei Stufen unterteilt ist:
__# --------------> The build image__
FROM node:latest AS build
RUN apt-get update && apt-get install -y --no-install-recommends dumb-init
ARG NPM_TOKEN
WORKDIR /usr/src/app
COPY package*.json /usr/src/app/
RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc && \
npm ci --only=production && \
rm -f .npmrc
__# --------------> The production image__
FROM node:20.9.0-bullseye-slim
ENV NODE_ENV production
COPY --from=build /usr/bin/dumb-init /usr/bin/dumb-init
USER node
WORKDIR /usr/src/app
COPY --chown=node:node --from=build /usr/src/app/node_modules /usr/src/app/node_modules
COPY --chown=node:node . /usr/src/app
CMD ["dumb-init", "node", "server.js"]
Wie Sie sehen, habe ich ein größeres Image für die build
-Stufe verwendet, da ich eventuell Tools wie gcc
(GNU Compiler Collection) zum Kompilieren nativer npm-Pakete oder für andere Anforderungen brauche.
Bei der zweiten Stufe gibt es eine besondere Notation für die COPY
-Anweisung, die den Ordner node_modules/
vom Build-Docker-Image in dieses neue Prod-Base-Image kopiert.
Ist Ihnen aufgefallen, dass NPM_TOKEN
als Build-Argument an das Intermediary-Docker-Imagebuild
übergeben wurde? Es ist in der Befehlsausgabe des docker history nodejs-tutorial
nicht mehr zu sehen, weil es nicht mehr in unserem Prod-Docker-Image existiert.
9. Unnötige Dateien in Node.js-Docker-Images vermeiden
Mit einer .gitignore
-Datei vermeiden Sie, dass unnötige oder womöglich vertrauliche Daten im Git-Repository abgelegt werden. Gleiches gilt für Docker-Images.
Was ist eine Docker-Ignore-Datei?
Bei Docker sorgt die Datei .dockerignore dafür, dass das Senden von darin enthaltenen Glob-Pattern-Matches an den Docker-Daemon übersprungen wird. Hier ist eine Liste von Dateien, damit Sie eine Vorstellung davon bekommen, was Sie womöglich in Ihr Docker-Image aufnehmen, aber idealerweise vermeiden sollten: .dockerignore
-node_modules
-npm-debug.log
-Dockerfile
-.git
-.gitignore
Wie Sie sehen, ist es ziemlich wichtig, node_modules/
zu überspringen. Hätten wir nämlich dies nicht ignoriert, hätte die vereinfachte Dockerfile-Version, mit der wir begonnen haben, dazu geführt, dass der lokale Ordner node_modules/
unverändert in den Container kopiert wird.
FROM node:20.9.0-bullseye-slim
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
CMD "npm" "start"
Für mehrstufige Docker-Builds ist eine .dockerignore
-Datei sogar noch wichtiger. Zur Erinnerung, wie der Docker-Build der zweiten Stufe aussieht:
__# --------------> The production image__
FROM node:20.9.0-bullseye-slim
ENV NODE_ENV production
COPY --from=build /usr/bin/dumb-init /usr/bin/dumb-init
USER node
WORKDIR /usr/src/app
COPY --chown=node:node --from=build /usr/src/app/node_modules /usr/src/app/node_modules
COPY --chown=node:node . /usr/src/app
CMD ["dumb-init", "node", "server.js"]
Warum ist es so wichtig, dass wir eine .dockerignore
haben? Ganz einfach: Wenn wir in der zweiten Dockerfile-Stufe den Befehl COPY . /usr/src/app
ausführen, kopieren wir damit auch alle lokalen node_modules/ in das Docker-Image. Das muss unbedingt vermieden werden, weil wir so auch modifizierten Quellcode im node_modules/
kopieren könnten.
Dazu kommt, dass wir beim Verwenden des Platzhalters COPY .
Gefahr laufen, vertrauliche Dateien in das Docker-Image zu übernehmen, die z. B. Anmeldedaten oder lokale Konfigurationen enthalten.
Meine wichtigen Tipps zur .dockerignore
-Datei:
Überspringen Sie womöglich geänderte Kopien von
node_modules/
im Docker-Image.Die Datei bewahrt Sie vor der Preisgabe von vertraulichen Daten wie Anmeldeinformationen, die in
.env
oderaws.json
enthalten sein können und so in das Node.js-Docker-Image gelangen.Damit können Sie schneller Docker-Builds entwickeln, weil Dateien ignoriert werden, die ansonsten eine Cache-Invalidierung verursachen. Wurde z. B. eine Log-File oder eine Konfigurationsdatei für eine lokale Umgebung modifiziert, würde dies den Docker-Image-Cache beim Kopieren über das lokale Verzeichnis ungültig machen.
10. Secrets im Docker-Build-Image mounten
Zur .dockerignore
-Datei ist anzumerken, dass dies ein Alles-oder-Nichts-Ansatz ist. Das bedeutet: Bei einem mehrstufigen Docker-Build können Sie die Datei nicht für jede Build-Stufe aktivieren oder deaktivieren.
Warum ist das wichtig? Idealerweise möchten wir die Datei .npmrc
in der Build-Stufe verwenden, da wir sie vielleicht brauchen werden, weil sie ein geheimes npm-Token für den Zugriff auf vertrauliche npm-Pakete enthält. Eventuell ist auch eine spezielle Proxy- oder Registry-Konfiguration notwendig, um Pakete abzurufen.
Daher ist es sinnvoll, in der build
-Stufe Zugriff auf die Datei .npmrc
zu haben. Allerdings können wir sie in der zweiten Stufe des Prod-Images überhaupt nicht gebrauchen, da sie vertrauliche Daten wie den geheimen npm-Token enthalten kann.
Um dieses Problems mit der.dockerignore
zu vermeiden, könnten wir ein lokales Dateisystem für die Build-Stufe mounten. Es gibt aber eine bessere Lösung.
Docker unterstützt eine relativ neue Funktion namens „Docker-Secrets“, die für unsere Anforderungen an die .npmrc
ideal ist. Und so funktioniert’s:
Wenn wir den Befehl
docker build
ausführen, geben wir bestimmte Befehlszeilen-Argumente an, die eine neue Secret-ID definieren und eine Datei als Secret-Quelle referenzieren.In der Dockerfile fügen wir Flags zur
RUN
-Anweisung hinzu, um die Prod-npm zu installieren. Diese bindet die Datei, auf die die Secret-ID referenziert, im Zielort ein: in der lokalen Verzeichnisdatei.npmrc
– also genau dort, wo wir sie haben wollen.Auf die
.npmrc
-Datei wird als Secret verwiesen, sie wird niemals in das Docker-Image kopiert.Zum Schluss müssen wir noch daran denken, die
.npmrc
-Datei zum Inhalt der Datei.dockerignore
hinzuzufügen, damit sie in kein Image – weder die Build- noch die Prod-Images – gelangt.
Prüfen wir einmal, wie das alles zusammen funktioniert. Sehen wir uns zuerst die Datei .dockerignore
an:
.dockerignore
node_modules
npm-debug.log
Dockerfile
.git
.gitignore
.npmrc
Betrachten wir nun die vollständige Dockerfile mit der aktualisierten RUN-Anweisung zur Installation der npm-Pakete, wenn der .npmrc
-Mount-Punkt angegeben wird:
__# --------------> The build image__
FROM node:latest AS build
RUN apt-get update && apt-get install -y --no-install-recommends dumb-init
WORKDIR /usr/src/app
COPY package*.json /usr/src/app/
RUN --mount=type=secret,mode=0644,id=npmrc,target=/usr/src/app/.npmrc npm ci --only=production
__# --------------> The production image__
FROM node:20.9.0-bullseye-slim
ENV NODE_ENV production
COPY --from=build /usr/bin/dumb-init /usr/bin/dumb-init
USER node
WORKDIR /usr/src/app
COPY --chown=node:node --from=build /usr/src/app/node_modules /usr/src/app/node_modules
COPY --chown=node:node . /usr/src/app
CMD ["dumb-init", "node", "server.js"]
Und schließlich der Befehl, der das Node.js-Docker-Image aufbaut:
$ docker build . -t nodejs-tutorial --secret id=npmrc,src=.npmrc
Hinweis: Secrets sind eine neue Docker-Funktion. Sollten Sie eine ältere Version verwenden, müssen Sie ggf. das Buildkit wie folgt aktivieren:
$ DOCKER_BUILDKIT=1 docker build . -t nodejs-tutorial --build-arg NPM_TOKEN=1234 --secret id=npmrc,src=.npmrc
Zusammenfassung
Geschafft! Sie haben jetzt ein optimiertes Node.js-Docker-Base-Image erstellt. Gut gemacht!
In diesem letzten Abschnitt fassen wir noch einmal den gesamten Leitfaden zur Containerisierung von Node.js-Docker-Webanwendungen zusammen. Dabei berücksichtigen wir auch leistungs- und sicherheitsbezogene Optimierungen, die für den Aufbau von produktionstauglichen Node.js-Docker-Images notwendig sind.
Weiterführende Ressourcen, die ich Ihnen zu diesem Thema empfehlen kann:
Nachdem Sie sichere, leistungsstarke Docker-Base-Images für Ihre Node.js-Anwendungen erstellt haben, sollten Sie mit dem kostenlosen Snyk-Konto Ihre Container auf Schwachstellen überprüfen.
Developer-First Security für Container
Mit Snyk identifizieren Sie Schwachstellen in Container-Images und Kubernetes-Workloads und adressieren sie automatisch.