Skip to main content

10 Best Practices für die Containerisierung von Node.js-Webanwendungen mit Docker

feature cheat sheet

15. September 2022

0 Min. Lesezeit

Suchen Sie nach Best Practices für das Erstellen von Node.js-Docker-Images für Ihre Webanwendungen? Dann sind Sie hier genau richtig!

Der folgende Artikel bietet produktionsreife Richtlinien für das Erstellen optimierter und sicherer Node.js-Docker-Images. Er ist unabhängig davon hilfreich, welche Node.js-Anwendung Sie entwickeln möchten. Dieser Artikel ist für Sie hilfreich, wenn:

  • Sie mithilfe der serverseitigen Rendering-Funktionen (SSR) von Node.js eine Frontend-Anwendung für React entwickeln möchten.

  • Sie nach Tipps suchen, wie Sie ein Node.js-Docker-Image für Ihre Microservices erstellen, die Fastify, NestJS oder andere Anwendungs-Frameworks verwenden.

Warum haben wir diesen Leitfaden zur Containerisierung von Node.js-Docker-Webanwendungen geschrieben?

Vielleicht wirkt es wie ein weiterer Artikel zum Erstellen von Docker-Images für Node.js-Anwendungen. Doch viele Beispiele, die wir in Blogs gesehen haben, sind sehr oberflächlich und erklären lediglich die Grundlagen dafür, wie ein Node.js-Docker-Image eine Anwendung ausführt – ohne Sicherheitsaspekte und Best Practices für das Erstellen von Node.js-Docker-Images sorgfältig zu berücksichtigen.

Wir lernen Schritt für Schritt, wie Sie Node.js-Webanwendungen containerisieren: angefangen bei einer einfachen, funktionierenden Dockerfile über das Verständnis der Fallstricke und Sicherheitslücken jeder Dockerfile-Anweisung bis hin zu deren Behebung.

Ein einfaches Node.js-Docker-Image

Die meisten Blogartikel, die wir gesehen haben, beginnen und enden mit grundlegenden Dockerfile-Anweisungen wie den folgenden zum Erstellen von Node.js-Docker-Images:

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

Kopieren Sie den Inhalt in eine Datei namens Dockerfile und erstellen Sie anschließend das Image und starten Sie es.

$ docker build . -t nodejs-tutorial
$ docker run -p 3000:3000 nodejs-tutorial

Es ist einfach und funktioniert.

Das einzige Problem? Es steckt voller Fehler und schlechter Praktiken für das Erstellen von Node.js-Docker-Images. Vermeiden Sie es unbedingt.

Verbessern wir diese Dockerfile, damit wir optimierte Node.js-Webanwendungen mit Docker erstellen können.

Sie können diesem Tutorial folgen, indem Sie dieses Repository klonen.

Befolgen Sie diese 10 Schritte, um optimierte Node.js-Webanwendungen mit Docker zu erstellen:

  1. Explizite und deterministische Docker-Basis-Image-Tags verwenden

  2. Nur Produktionsabhängigkeiten im Node.js-Docker-Image installieren

  3. Node.js-Tools für die Produktion optimieren

  4. Container nicht als Root ausführen

  5. Node.js-Docker-Webanwendungen sicher beenden

  6. Graceful Shutdown für Ihre Node.js-Webanwendungen

  7. Sicherheitslücken in Ihrem Node.js-Docker-Image finden und beheben

  8. Multi-Stage-Builds verwenden

  9. Unnötige Dateien aus Ihren Node.js-Docker-Images heraushalten

  10. Secrets in das Docker-Build-Image einbinden

1. Explizite und deterministische Docker-Basis-Image-Tags verwenden

Es mag naheliegend erscheinen, Ihr Image auf dem node-Docker-Image aufzubauen. Doch was ziehen Sie beim Erstellen des Images tatsächlich herunter? Docker-Images werden immer über Tags referenziert. Wenn Sie kein Tag angeben, wird standardmäßig das Tag :latest verwendet.

Wenn Sie also Folgendes in Ihrer Dockerfile angeben, erstellen Sie immer die neueste Version des Docker-Images, das von der Node.js Docker Working Group erstellt wurde:

FROM node

Das Erstellen auf Grundlage des standardmäßigen node-Images hat folgende Nachteile:

  1. Docker-Image-Builds sind inkonsistent. So wie wir lockfiles verwenden, um bei jeder Installation von npm-Paketen ein deterministisches Verhalten von npm install zu erzielen, möchten wir auch deterministische Docker-Image-Builds erhalten. Wenn wir das Image auf Grundlage von node erstellen – was effektiv dem Tag node:latest entspricht –, lädt jeder Build ein neu erstelltes Docker-Image von node herunter. Ein solches nicht deterministisches Verhalten möchten wir vermeiden.

  2. Das node-Docker-Image basiert auf einem vollwertigen Betriebssystem mit zahlreichen Bibliotheken und Tools, die Sie möglicherweise für den Betrieb Ihrer Node.js-Webanwendung benötigen oder auch nicht. Das hat zwei Nachteile. Erstens bedeutet ein größeres Image eine größere Downloadmenge. Dadurch steigt nicht nur der Speicherbedarf, sondern es dauert auch länger, das Image herunterzuladen und neu zu erstellen. Zweitens können Sie damit Sicherheitslücken, die in diesen Bibliotheken und Tools vorhanden sein können, in Ihr Image einführen.

Tatsächlich ist das node-Docker-Image ziemlich groß und enthält Hunderte von Sicherheitslücken verschiedener Arten und Schweregrade. Wenn Sie es verwenden, starten Sie standardmäßig mit einer Basis von 642 Sicherheitslücken und Hunderten Megabyte an Image-Daten, die bei jedem Abruf und Build heruntergeladen werden.

Balkendiagramm zum Vergleich von Sicherheitslücken in offiziellen Container-Images in den Jahren 2018 und 2019 für node, postgres, nginx, httpd, mongo, mysql, couchbase, memcached und

Für bessere Docker-Images empfehlen wir Folgendes:

  1. Verwenden Sie kleine Docker-Images. Das reduziert den Softwareumfang im Docker-Image und damit potenzielle Angriffsvektoren. Zudem fällt die Image-Größe geringer aus, was den Image-Build beschleunigt.

  2. Verwenden Sie den Docker-Image-Digest, also den statischen SHA256-Hash des Images. So stellen Sie sicher, dass Docker-Image-Builds auf Grundlage des Basis-Images deterministisch sind.

Wir haben einen ausführlichen Artikel darüber geschrieben, wie Sie das beste Node.js-Docker-Image auswählen. Darin erfahren Sie, warum eine aktuelle Debian-Slim-Distribution mit einer Node.js-Runtime-Version mit Long-Term-Support die ideale Wahl ist.

Wir empfehlen folgendes Node.js-Docker-Image:

FROM node:20.9.0-bullseye-slim

Dieses Node.js-Docker-Image-Tag verwendet eine bestimmte Version der Node.js-Runtime (20.9.0), die der aktuellsten Long-Term-Support-Version entspricht. Es verwendet die Image-Variante bullseye, die der aktuellen stabilen Version Debian 11 mit einem ausreichend weit in der Zukunft liegenden Support-Ende entspricht. Schließlich wird die Image-Variante slim verwendet, um den Softwareumfang des Betriebssystems zu reduzieren. So bleibt das Image einschließlich Node.js-Runtime und Tools kleiner als 200 MB.

Eine häufige, auf fehlendem Wissen beruhende Praxis in Tutorials und Leitfäden ist die Verwendung der folgenden Docker-Anweisung für ein Basis-Image:

FROM node:alpine

In diesen Artikeln wird die Verwendung des Node.js-Alpine-Docker-Images empfohlen. Doch ist das wirklich ideal? Die Empfehlung beruht meist darauf, dass das Node.js-Alpine-Docker-Image einen geringeren Softwareumfang hat. Es unterscheidet sich jedoch in anderen Merkmalen erheblich und ist daher kein optimales Produktions-Basis-Image für Node.js-Anwendungs-Runtimes.

Was ist Node Alpine?

Node.js Alpine ist ein inoffizielles Docker-Container-Image, das vom Node.js-Docker-Team gepflegt wird. Das Node.js-Image bündelt das Alpine-Betriebssystem, das auf den minimalen Software-Tools busybox und der C-Bibliotheksimplementierung musl basiert. Diese beiden Merkmale des Node.js-Alpine-Images tragen dazu bei, dass es vom Node.js-Team inoffiziell unterstützt wird. Außerdem können viele Scanner für Sicherheitslücken Software-Artefakte oder Runtimes in Node.js-Alpine-Images nicht ohne Weiteres erkennen. Das wirkt den Bemühungen entgegen, Ihre Container-Images zu schützen.

Auch wenn Sie das Node.js-Alpine-Image-Tag verwenden: Eine Basis-Image-Anweisung in Form eines Alias-Worts kann weiterhin neue Builds dieses Tags abrufen, da Docker-Image-Tags veränderlich sind. Den SHA256-Hash finden Sie im Docker Hub für dieses Node.js-Tag oder, nachdem Sie dieses Image lokal abgerufen haben, mit dem folgenden Befehl im Ausgabefeld Digest:

$ 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

Eine weitere Möglichkeit, den SHA256-Hash zu finden, ist die Ausführung des folgenden Befehls:

$ docker images --digests
REPOSITORY                                   TAG                     DIGEST                                                                    IMAGE ID       CREATED         SIZE
node                                         20.9.0-bullseye-slim    sha256:330fa0342b6ad2cbdab30ac44195660af5a1f298cc499d8cbdf7496b02ea17d8   9ea15fe618bd   7 days ago      200MB

Nun 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 jedoch nur den Namen des Node.js-Docker-Images an, nicht aber ein Image-Tag. Dadurch ist unklar, welches Image-Tag genau verwendet wird. Die Angabe ist nicht lesbar, schwer zu warten und sorgt für keine gute Developer Experience.

Beheben wir das, indem wir das Dockerfile aktualisieren und das vollständige Basis-Image-Tag für die Node.js-Version angeben, die diesem SHA256-Hash entspricht:

FROM node:20.9.0-bullseye-slim@sha256:330fa0342b6ad2cbdab30ac44195660af5a1f298cc499d8cbdf7496b02ea17d8
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
CMD "npm" "start"

Der Docker-Image-Digest gewährleistet ein deterministisches Image, kann jedoch für manche Image-Scanning-Tools verwirrend oder kontraproduktiv sein, wenn sie ihn nicht interpretieren können. Deshalb ist eine explizite Node.js-Runtime-Version wie 20.9.0 vorzuziehen. Auch wenn sie theoretisch veränderlich und überschreibbar ist, werden Sicherheitsupdates und andere Aktualisierungen in der Praxis in einer neuen Version wie 20.9.1 bereitgestellt. Daher können Sie von ausreichend deterministischen Builds ausgehen.

Unsere finale Dockerfile-Empfehlung für diesen Schritt sieht daher so aus:

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 das Erstellen sicherer Container-Images.

2. Nur Produktionsabhängigkeiten im Node.js-Docker-Image installieren

Die folgende Dockerfile-Anweisung installiert alle Abhängigkeiten im Container, einschließlich devDependencies, die für den Betrieb einer funktionsfähigen Anwendung nicht benötigt werden. Dadurch entsteht ein unnötiges Sicherheitsrisiko durch Pakete, die als Entwicklungsabhängigkeiten verwendet werden, und das Image wird unnötig größer.

RUN npm install

Wenn Sie meinem vorherigen Leitfaden zu 10 Best Practices für npm-Sicherheit gefolgt sind, wissen Sie, dass Sie mit npm ci deterministische Builds sicherstellen sollten. So vermeiden Sie Überraschungen in einem Continuous-Integration-(CI-)Ablauf, denn der Vorgang wird abgebrochen, wenn es Abweichungen von der Lockdatei gibt.

Beim Erstellen eines Docker-Images für die Produktion möchten wir sicherstellen, dass ausschließlich Produktionsabhängigkeiten auf deterministische Weise installiert werden. Daraus ergibt sich folgende Best Practice für die Installation von npm-Abhängigkeiten in einem Container-Image:

RUN npm ci --only=production

Die aktualisierte Dockerfile sieht in diesem Schritt wie folgt aus:

FROM node:20.9.0-bullseye-slim
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm ci --only=production
CMD "npm" "start"

Weitere Informationen finden Sie in unserem Artikel über Softwareabhängigkeiten.

3. Node.js-Tools für die Produktion optimieren

Wenn Sie Ihr Node.js-Docker-Image für die Produktion erstellen, sollten Sie sicherstellen, dass alle Frameworks und Bibliotheken optimal für Leistung und Sicherheit konfiguriert sind.

Daher fügen wir die folgende Dockerfile-Anweisung hinzu:

ENV NODE_ENV production

Auf den ersten Blick wirkt das überflüssig, da wir bereits in der Phase npm install ausschließlich Produktionsabhängigkeiten angegeben haben. Warum ist das also nötig?

Entwicklerinnen und Entwickler bringen die Einstellung der Umgebungsvariablen NODE_ENV=production meist mit der Installation produktionsbezogener Abhängigkeiten in Verbindung. Diese Einstellung hat jedoch noch weitere Auswirkungen, die Sie kennen sollten.

Manche Frameworks und Bibliotheken aktivieren möglicherweise nur dann die für die Produktion optimierte Konfiguration, wenn die Umgebungsvariable NODE_ENV auf production gesetzt ist. Unabhängig davon, ob wir diese Praxis bei Frameworks für gut oder schlecht halten, ist es wichtig, sie zu kennen.

Die Express-Dokumentation erläutert beispielsweise, wie wichtig es ist, diese Umgebungsvariable zu setzen, um leistungs- und sicherheitsbezogene Optimierungen zu aktivieren:

Express-Dokumentation mit hervorgehobenen Vorteilen von NODE_ENV="production": zwischengespeicherte Vorlagen und CSS sowie weniger ausführliche Fehlermeldungen.

Die Auswirkungen der Variablen NODE_ENV auf die Leistung können erheblich sein.

Die Fachleute von Dynatrace haben einen Blogbeitrag zu den drastischen Auswirkungen des Fehlens von NODE_ENV in Ihren Express-Anwendungen veröffentlicht.

Viele weitere Bibliotheken, auf die Sie sich verlassen, erwarten möglicherweise ebenfalls, dass diese Variable gesetzt ist. Daher sollten wir sie in unserer Dockerfile festlegen.

Die aktualisierte Dockerfile sollte nun wie folgt aussehen und die Einstellung der Umgebungsvariable NODE_ENV enthalten:

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 Sicherheitsprinzip aus den frühen Tagen von Unix. Wir sollten es stets befolgen, wenn wir unsere containerisierten Node.js-Webanwendungen ausführen.

Die Bedrohungsbewertung ist ziemlich eindeutig: Kann ein Angreifer die Webanwendung so kompromittieren, dass Command Injection oder Directory Path Traversal möglich wird, werden diese Befehle mit den Rechten des Benutzers ausgeführt, dem der Anwendungsprozess gehört. Ist dieser Prozess Root, kann der Angreifer im Container praktisch alles tun – auch einen Container Escape versuchen oder eine Rechteausweitung durchführen. Warum sollten wir das riskieren? Sie haben recht: Das sollten wir nicht.

Sagen Sie es mir nach: „Freunde lassen Freunde Container nicht als Root ausführen!“

Das offizielle node-Docker-Image und seine Varianten wie alpine enthalten einen Benutzer mit minimalen Berechtigungen, der denselben Namen trägt: node. Es reicht jedoch nicht aus, den Prozess einfach als node auszuführen. Für die Funktionalität der Anwendung ist beispielsweise Folgendes nicht optimal:

USER node
CMD "npm" "start"

Der Grund dafür ist, dass die Dockerfile-Anweisung USER lediglich sicherstellt, dass der Prozess dem Benutzer node gehört. Was ist mit all den Dateien, die wir zuvor mit der Anweisung COPY kopiert haben? Sie gehören root. So funktioniert Docker standardmäßig.

So entziehen Sie dem Prozess vollständig und korrekt seine Berechtigungen. Gleichzeitig sehen Sie hier unsere bis zu diesem Punkt aktualisierten Dockerfile-Best Practices:

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-Webanwendungen in Docker sicher beenden

Einer der häufigsten Fehler, die mir in Blogs und Artikeln zum Containerisieren von Node.js-Anwendungen in Docker-Containern begegnen, betrifft den Aufruf des Prozesses. Die folgenden Muster und ihre Varianten sind ungeeignet und sollten vermieden werden:

  • CMD “npm” “start”

  • CMD [“yarn”, “start”]

  • CMD “node” “server.js”

  • CMD “start-app.sh”

Sehen wir uns das genauer an! Ich führe Sie durch die fehlerhaften Methoden zum Aufruf des Prozesses und erkläre, warum Sie sie vermeiden sollten.

Die folgenden Punkte sind entscheidend, um zu verstehen, wie Node.js-Anwendungen in Docker-Containern korrekt ausgeführt und beendet werden:

  1. Eine Orchestrierungs-Engine wie Docker Swarm, Kubernetes oder auch die Docker-Engine selbst muss Signale an den Prozess im Container senden können. Dabei handelt es sich meist um Signale zum Beenden einer Anwendung, etwa SIGTERM und SIGKILL.

  2. Der Prozess wird möglicherweise indirekt ausgeführt. In diesem Fall ist nicht immer gewährleistet, dass er diese Signale empfängt.

  3. Der Linux-Kernel behandelt Prozesse mit der Prozess-ID 1 (PID 1) anders als alle anderen Prozesse.

Mit diesem Wissen sehen wir uns nun an, wie sich ein Prozess für einen Container aufrufen lässt. Wir beginnen mit dem Beispiel aus der Dockerfile, die wir erstellen:

CMD "npm" "start"

Dabei gibt es zwei Einschränkungen. Erstens führen wir die Node-Anwendung indirekt aus, indem wir direkt den npm-Client aufrufen. Wer sagt, dass die npm-CLI alle Ereignisse an die Node-Laufzeitumgebung weiterleitet? Das tut sie tatsächlich nicht – und das lässt sich leicht testen.

Richten Sie in Ihrer Node.js-Anwendung einen Event-Handler für das Signal SIGHUP ein, der jedes Mal eine Meldung in der Konsole protokolliert, wenn Sie ein Ereignis senden. Ein einfaches Codebeispiel könnte so aussehen:

function handle(signal) {
   console.log(`*^!@4=> Received event: ${signal}`)
}
process.on('SIGHUP', handle)

Starten Sie dann den Container. Sobald er läuft, senden Sie ihm mit der docker-CLI und dem speziellen Befehlszeilen-Flag --signal gezielt das Signal SIGHUP:

$ docker kill --signal=SIGHUP elastic_archimedes

Nichts passiert, oder? Das liegt daran, dass der npm-Client keine Signale an den Node-Prozess weiterleitet, den er gestartet hat.

Die zweite Einschränkung betrifft die verschiedenen Möglichkeiten, die CMD-Anweisung in der Dockerfile anzugeben. Es gibt zwei Varianten, die sich unterscheiden:

  1. die Shell-Form, bei der der Container einen Shell-Interpreter startet, der den Prozess umschließt. In solchen Fällen leitet die Shell Signale möglicherweise nicht korrekt an Ihren Prozess weiter.

  2. die Exec-Form, bei der ein Prozess direkt und ohne Shell gestartet wird. Sie wird in der JSON-Array-Notation angegeben, zum Beispiel: CMD [“npm”, “start”]. Alle an den Container gesendeten Signale werden direkt an den Prozess weitergeleitet.

Mit diesem Wissen können wir die Anweisung zur Prozessausführung in unserer Dockerfile wie folgt verbessern:

CMD ["node", "server.js"]

Wir rufen den Node-Prozess jetzt direkt auf. So stellen wir sicher, dass er alle an ihn gesendeten Signale empfängt und nicht von einem Shell-Interpreter umschlossen wird.

Das birgt jedoch eine weitere Falle.

Wenn Prozesse als PID 1 ausgeführt werden, übernehmen sie faktisch einige Aufgaben eines Init-Systems, das normalerweise ein Betriebssystem und dessen Prozesse initialisiert. Der Kernel behandelt PID 1 anders als andere Prozess-IDs. Aufgrund dieser Sonderbehandlung löst ein SIGTERM-Signal an einen laufenden Prozess nicht das standardmäßige Verhalten aus, den Prozess zu beenden, wenn dieser keinen eigenen Handler dafür eingerichtet hat.

Dazu heißt es in der Empfehlung der Node.js Docker-Arbeitsgruppe: „Node.js wurde nicht für die Ausführung als PID 1 entwickelt. Das führt bei der Ausführung in Docker zu unerwartetem Verhalten. Ein Node.js-Prozess, der als PID 1 läuft, reagiert beispielsweise nicht auf SIGINT (STRG+C) und ähnliche Signale.“

Verwenden Sie dazu ein Tool, das wie ein Init-Prozess agiert: Es wird mit PID 1 aufgerufen und startet dann unsere Node.js-Anwendung als weiteren Prozess. Dabei leitet es alle Signale an diesen Node.js-Prozess weiter. Wenn möglich, sollte das Tool so klein wie möglich sein, damit das Sicherheitsrisiko durch zusätzliche Komponenten im Container-Image gering bleibt.

Ein solches Tool, das wir bei Snyk verwenden, ist dumb-init. Es ist statisch gelinkt und hat nur einen geringen Platzbedarf. So richten wir es ein:

RUN apt-get update && apt-get install -y --no-install-recommends dumb-init
CMD ["dumb-init", "node", "server.js"]

Damit sieht unsere bis hierhin aktualisierte Dockerfile wie folgt aus. Sie werden sehen, dass wir dumb-init direkt nach der Image-Deklaration installieren. So nutzen wir das Caching der Docker-Layer:

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 Software mit der Docker-Anweisung RUN hinzufügen, wie mit RUN apt-get update && apt-get install, bleiben Informationen im Docker-Image zurück. Wir können den Befehl wie folgt erweitern, um aufzuräumen und das Docker-Image schlanker zu halten:

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"]

Wir können das noch verbessern, indem wir dumb-init über die Dockerfile-Anweisung ENTRYPOINT festlegen und ihm den auszuführenden Node.js-Prozess als Befehlszeilenargument übergeben. Aktualisieren Sie dazu den entsprechenden Eintrag in der Dockerfile wie folgt:

FROM node:16.17.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
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["node", "server.js"]

Tipp: Noch besser ist es, das Tool dumb-init in einer früheren Build-Stage zu installieren und anschließend die resultierende Datei /usr/bin/dumb-init in das finale Container-Image zu kopieren. So bleibt das Image sauber. Im weiteren Verlauf dieser Anleitung erfahren Sie mehr über Docker-Builds mit mehreren Stages.

Gut zu wissen: Die Befehle docker kill und docker stop senden Signale nur an den Container-Prozess mit PID 1. Wenn Sie ein Shell-Skript ausführen, das Ihre Node.js-Anwendung startet, beachten Sie: Eine Shell-Instanz wie /bin/sh leitet Signale nicht an untergeordnete Prozesse weiter. Ihre App erhält daher niemals ein SIGTERM-Signal.

6. Node.js-Webanwendungen kontrolliert herunterfahren

Wenn wir schon über Signale zum Beenden von Anwendungen sprechen, sollten wir auch sicherstellen, dass sie ordnungsgemäß und kontrolliert heruntergefahren werden, ohne Nutzerinnen und Nutzer zu beeinträchtigen.

Empfängt eine Node.js-Anwendung ein Unterbrechungssignal, auch SIGINT oder CTRL+C genannt, wird der Prozess abrupt beendet – sofern keine Event-Handler für ein anderes Verhalten eingerichtet wurden. Dadurch werden verbundene Clients der Webanwendung sofort getrennt. Stellen Sie sich nun Hunderte von Node.js-Webcontainern vor, die von Kubernetes orchestriert werden und je nach Skalierungsbedarf oder zur Fehlerbehebung gestartet und beendet werden. Das ist kein besonders gutes Nutzererlebnis.

Sie können dieses Problem ganz einfach simulieren. Hier sehen Sie eine einfache Fastify-Webanwendung mit einem Endpunkt, der absichtlich erst nach 60 Sekunden antwortet:

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()

Starten Sie die Anwendung und senden Sie, sobald sie läuft, eine einfache HTTP-Anfrage an diesen Endpunkt:

$ time curl https://localhost:3000/delayed

Drücken Sie im laufenden Node.js-Konsolenfenster CTRL+C. Sie werden sehen, dass die curl-Anfrage abrupt beendet wird. Genau so erleben es Ihre Nutzerinnen und Nutzer, wenn Container heruntergefahren werden.

Für ein besseres Nutzererlebnis können wir Folgendes tun:

  1. Richten Sie einen Event-Handler für die verschiedenen Beendigungssignale wie SIGINT und SIGTERM ein.

  2. Der Handler wartet, bis Aufräumarbeiten abgeschlossen sind, etwa das Schließen von Datenbankverbindungen und laufenden HTTP-Anfragen.

  3. Anschließend beendet der Handler den Node.js-Prozess.

Bei Fastify kann unser Handler speziell fastify.close() aufrufen und auf das zurückgegebene Promise warten. Fastify sorgt außerdem dafür, dass neue Verbindungen mit dem HTTP-Statuscode 503 beantwortet werden, um zu signalisieren, dass die Anwendung nicht verfügbar ist.

Fügen wir 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)

Zugegeben: Das betrifft eher Webanwendungen allgemein als Dockerfiles. In orchestrierten Umgebungen ist es jedoch umso wichtiger.

7. Sicherheitslücken in Ihrem Node.js-Docker-Image finden und beheben

Erinnern Sie sich daran, wie wichtig kleine Docker-Basis-Images für unsere Node.js-Anwendungen sind? Setzen wir das in die Praxis um.

Ich verwende die Snyk CLI, um unser Docker-Image zu testen. Hier können Sie sich kostenlos für ein Snyk-Konto registrieren.

$ npm install -g snyk
$ snyk auth
$ snyk container test node:20.9.0-bullseye-slim --file=Dockerfile

Mit dem ersten Befehl wird die Snyk CLI installiert. Anschließend melden Sie sich über die Befehlszeile an, um einen API-Schlüssel abzurufen. Danach können wir den Container auf Sicherheitsprobleme prüfen. Das ist 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 Betriebssystemabhängigkeiten erkannt, darunter die ausführbare Datei der Node.js-Laufzeitumgebung, und keine anfälligen Versionen der Laufzeitumgebung gefunden. Im Container-Image gibt es jedoch 44 Sicherheitslücken in einigen Softwarekomponenten. 43 dieser Abhängigkeiten weisen Sicherheitslücken mit niedrigem Schweregrad auf. Eine kritische Sicherheitslücke betrifft die 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

Sicherheitslücken in Docker-Images beheben

Eine schnelle und wirksame Möglichkeit, die Software in Ihrem Docker-Image sicher zu halten, ist, das Image neu zu erstellen. Dabei sind Sie darauf angewiesen, dass das verwendete Docker-Basis-Image die entsprechenden Updates bereitstellt. Alternativ können Sie Betriebssystempakete explizit aktualisieren und so auch Sicherheitskorrekturen installieren.

Beim offiziellen Node.js-Docker-Image kann es etwas länger dauern, bis das Team Aktualisierungen bereitstellt. Daher bringt es nichts, das Node.js-Docker-Image 20.9.0-bullseye-slim oder lts-bullseye-slim neu zu erstellen. Alternativ können Sie Ihr eigenes Basis-Image mit aktueller Software von Debian verwalten. In unserer Dockerfile geht das so:

RUN apt-get update && apt-get upgrade -y

Führen wir nach dem Erstellen des Node.js-Docker-Images mit der neu hinzugefügten RUN-Anweisung den Snyk-Sicherheitsscan 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

Das Ergebnis: eine zusätzliche Betriebssystemabhängigkeit (98 statt zuvor 97). Alle 43 Sicherheitslücken, die dieses Node.js-Docker-Image betreffen, weisen nun jedoch einen niedrigen Schweregrad auf. Außerdem haben wir die kritische Sicherheitslücke in zlib behoben. Ein großartiges Ergebnis!

Was wäre, wenn wir die Basis-Image-Anweisung FROM node verwendet hätten? Noch besser: Nehmen wir an, Sie hätten ein spezifischeres Node.js-Docker-Basis-Image verwendet, etwa dieses:

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

Eine bestimmte Node.js-Laufzeitversion wie FROM node:14.2.0-slim scheint vielleicht auszureichen, da Sie eine konkrete Version (14.2.0) und ein kleines Container-Image (durch das Image-Tag slim) angegeben haben. Snyk kann Sicherheitslücken jedoch anhand von zwei Hauptquellen erkennen:

  1. Die Node.js-Laufzeitumgebung selbst: Sind Ihnen die beiden führenden Sicherheitslücken im obigen Bericht aufgefallen? Dabei handelt es sich um öffentlich bekannte Sicherheitsprobleme in der Node.js-Laufzeitumgebung. Die sofortige Abhilfe besteht darin, auf eine neuere Node.js-Version zu aktualisieren. Snyk nennt Ihnen sowohl die passende Version als auch die Version, in der die Lücke behoben wurde – hier 14.11.0, wie Sie in der Ausgabe sehen.

  2. Die Tools und Bibliotheken, die in diesem Debian-Basis-Image installiert sind, etwa glibc, bzip2, gcc, perl, bash, tar, libcrypt und andere. Diese anfälligen Versionen im Container stellen zwar möglicherweise keine unmittelbare Gefahr dar. Aber warum sollten Sie sie behalten, wenn Sie sie nicht verwenden?

Das Beste an diesem Bericht der Snyk CLI? Snyk empfiehlt auch alternative Basis-Images, auf die Sie umsteigen können. Sie müssen also nicht selbst danach suchen. Die Suche nach Alternativen kann viel Zeit kosten – Snyk nimmt Ihnen diese Arbeit ab.

Meine Empfehlung an diesem Punkt lautet:

  1. Wenn Sie Ihre Docker-Images in einer Registry wie Docker Hub oder Artifactory verwalten, können Sie sie ganz einfach in Snyk importieren. Die Plattform findet dann die Sicherheitslücken für Sie. Außerdem erhalten Sie in der Snyk-Oberfläche Empfehlungen und können Ihre Docker-Images fortlaufend auf neu entdeckte Sicherheitslücken überwachen.

  2. Verwenden Sie die Snyk CLI in Ihrer CI-Automatisierung. Die CLI ist sehr flexibel – genau deshalb haben wir sie entwickelt: damit Sie sie in beliebige benutzerdefinierte Workflows integrieren können. Wenn Sie möchten, können Sie auch Snyk for GitHub Actions verwenden.

Weitere Möglichkeiten, Schwachstellen in Container-Images zu verwalten, finden Sie in unserem Leitfaden zu Container-Sicherheit.

8. Multi-Stage-Builds verwenden

Multi-Stage-Builds sind eine hervorragende Möglichkeit, von einer einfachen, aber potenziell fehleranfälligen Dockerfile zu getrennten Schritten beim Erstellen eines Docker-Images überzugehen. So vermeiden wir, dass vertrauliche Informationen offengelegt werden. Außerdem können wir ein größeres Docker-Basis-Image verwenden, um unsere Abhängigkeiten zu installieren und gegebenenfalls native npm-Pakete zu kompilieren, und anschließend alle diese Artefakte in ein kleines Produktions-Basis-Image wie unser Alpine-Beispiel kopieren.

Offenlegung vertraulicher Informationen verhindern

Der Anwendungsfall, bei dem es darum geht, die Offenlegung vertraulicher Informationen zu vermeiden, kommt häufiger vor, als Sie denken.

Wenn Sie Docker-Images für die Arbeit erstellen, verwalten Sie wahrscheinlich auch private npm-Pakete. In diesem Fall mussten Sie vermutlich einen Weg finden, das geheime NPM_TOKEN für npm install verfügbar zu machen.

Hier ein Beispiel dafür, was ich meine:

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"]

Dadurch bleibt die Datei .npmrc jedoch mit dem geheimen npm-Token im Docker-Image zurück. Sie könnten versuchen, das zu verbessern, indem Sie sie anschließend löschen, etwa so:

RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc && \
   npm ci --only=production
RUN rm -rf .npmrc

Jetzt ist die Datei .npmrc jedoch in einer anderen Ebene des Docker-Images verfügbar. Wenn dieses Docker-Image öffentlich ist oder jemand anderweitig darauf zugreifen kann, ist Ihr Token kompromittiert. Besser wäre es folgendermaßen:

RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc && \
   npm ci --only=production; \
   rm -rf .npmrc

Das Problem ist nun, dass die Dockerfile selbst als vertrauliche Ressource behandelt werden muss, da sie das geheime npm-Token enthält.

Zum Glück unterstützt Docker eine 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

Anschließend erstellen wir das Image wie folgt:

$ docker build . -t nodejs-tutorial --build-arg NPM_TOKEN=1234

Ich weiß, Sie denken, dass wir an diesem Punkt fertig sind, aber leider muss ich Sie enttäuschen.

So ist das mit Sicherheit: Manchmal ist das Offensichtliche nur eine weitere Falle.

Was ist jetzt das Problem, fragen Sie sich? Auf diese Weise an Docker übergebene Build-Argumente werden im Verlaufsprotokoll gespeichert. Sehen wir es uns selbst an. Führen Sie diesen Befehl aus:

$ docker history nodejs-tutorial

Die Ausgabe sieht so aus:

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.

Es gibt eine hervorragende Möglichkeit, Geheimnisse für das Container-Image zu verwalten. Doch jetzt ist der richtige Zeitpunkt, Multi-Stage-Builds als Abhilfe für dieses Problem einzuführen und zu zeigen, wie sich minimale Images erstellen lassen.

Multi-Stage-Builds für Node.js-Docker-Images

Wie beim Prinzip der Separation of Concerns in der Softwareentwicklung wenden wir dieselben Ideen an, um unsere Node.js-Docker-Images zu erstellen. Wir verwenden ein Image, um alles zu erstellen, was die Node.js-Anwendung zum Ausführen benötigt. In der Node.js-Welt bedeutet das, npm-Pakete zu installieren und bei Bedarf native npm-Module zu kompilieren. Das ist unsere erste Build-Stufe.

Das zweite Docker-Image, das die zweite Build-Stufe darstellt, ist das Docker-Produktions-Image. Diese zweite und letzte Stufe ist das Image, das wir tatsächlich optimieren und – sofern vorhanden – in einer Registry veröffentlichen. Das erste Image, das wir als build-Image bezeichnen, wird verworfen und bleibt als verwaistes Image auf dem Docker-Host zurück, auf dem es erstellt wurde, bis es bereinigt wird.

Hier sehen Sie die aktualisierte Dockerfile, die unseren bisherigen Fortschritt zeigt und 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 für die build-Stufe ein größeres Image gewählt, da ich möglicherweise Tools wie gcc (die GNU Compiler Collection) zum Kompilieren nativer npm-Pakete oder für andere Aufgaben benötige.

In der zweiten Stufe gibt es eine spezielle Syntax für die COPY-Anweisung, mit der der Ordner node_modules/ aus dem Build-Docker-Image in dieses neue Produktions-Basis-Image kopiert wird.

Sehen Sie jetzt auch, dass NPM_TOKEN als Build-Argument an das zwischengeschaltete Docker-Image build übergeben wird? In der Ausgabe des Befehls docker history nodejs-tutorial ist es nicht mehr zu sehen, da es in unserem Docker-Produktions-Image nicht vorhanden ist.

9. Unnötige Dateien aus Ihren Node.js-Docker-Images heraushalten

Sie haben eine .gitignore-Datei, damit keine unnötigen und möglicherweise vertraulichen Dateien in Ihr Git-Repository gelangen, richtig? Dasselbe gilt für Docker-Images.

Was ist eine Docker-Ignore-Datei?

Docker verfügt über eine .dockerignore-Datei. Damit werden alle darin aufgeführten Glob-Muster vom Senden an den Docker-Daemon ausgeschlossen. Die folgende Dateiliste zeigt, was Sie möglicherweise in Ihr Docker-Image aufnehmen und besser vermeiden sollten:- .dockerignore- node_modules- npm-debug.log- Dockerfile- .git- .gitignore

Wie Sie sehen, ist es besonders wichtig, node_modules/ auszuschließen. Andernfalls hätte die einfache Dockerfile, mit der wir begonnen haben, den lokalen Ordner node_modules/ unverändert in den Container kopiert.

FROM node:20.9.0-bullseye-slim
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
CMD "npm" "start"

Eine .dockerignore-Datei ist sogar noch wichtiger, wenn Sie Multi-Stage-Docker-Builds verwenden. Zur Erinnerung: So sieht die Docker-Build-Datei der zweiten Stufe aus:

__# --------------> 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"]

Eine .dockerignore-Datei ist wichtig, weil wir mit COPY . /usr/src/app in der zweiten Dockerfile-Stufe auch alle lokalen node_modules/ in das Docker-Image kopieren. Das sollten Sie unbedingt vermeiden, denn dabei könnte geänderter Quellcode aus node_modules/ kopiert werden.

Außerdem könnten wir mit dem Platzhalter COPY . vertrauliche Dateien mit Anmeldedaten oder lokaler Konfiguration in das Docker-Image kopieren.

Das Wichtigste zur .dockerignore-Datei:

  • Verhindert, dass potenziell geänderte Kopien von node_modules/ im Docker-Image landen.

  • Schützt vor der Offenlegung von Geheimnissen, etwa wenn Anmeldedaten aus Dateien wie .env oder aws.json in das Node.js-Docker-Image gelangen.

  • Beschleunigt Docker-Builds, da Dateien ignoriert werden, die andernfalls eine Cache-Invalidierung verursachen würden. Eine geänderte Protokolldatei oder lokale Umgebungskonfigurationsdatei hätte beispielsweise dazu geführt, dass der Docker-Image-Cache auf der Ebene ungültig wird, auf der das lokale Verzeichnis kopiert wird.

10. Geheimnisse in das Docker-Build-Image einbinden

Wichtig zu wissen: Die .dockerignore-Datei verfolgt einen Alles-oder-nichts-Ansatz und lässt sich bei einem Multi-Stage-Docker-Build nicht für einzelne Build-Stufen aktivieren oder deaktivieren.

Warum ist das wichtig? Idealerweise würden wir die Datei .npmrc in der Build-Stufe verwenden, da wir sie möglicherweise benötigen, um mit einem geheimen npm-Token auf private npm-Pakete zuzugreifen. Vielleicht enthält sie auch eine bestimmte Proxy- oder Registry-Konfiguration, über die Pakete bezogen werden.

Das bedeutet, dass die Datei .npmrc in der build-Stufe verfügbar sein sollte. In der zweiten Stufe für das Produktions-Image benötigen wir sie jedoch überhaupt nicht und möchten sie dort auch nicht haben, da sie vertrauliche Informationen wie das geheime npm-Token enthalten kann.

Eine Möglichkeit, diese Einschränkung der .dockerignore-Datei zu umgehen, besteht darin, ein lokales Dateisystem einzubinden, das in der Build-Stufe verfügbar ist. Es gibt jedoch eine bessere Lösung.

Docker unterstützt eine relativ neue Funktion namens Docker Secrets, die sich für unseren Anwendungsfall mit .npmrc bestens eignet. So funktioniert sie:

  • Beim Ausführen des Befehls docker build geben wir Befehlszeilenargumente an, die eine neue Secret-ID definieren und eine Datei als Quelle des Geheimnisses festlegen.

  • In der Dockerfile fügen wir der RUN-Anweisung Flags hinzu, um die Produktionsversion von npm zu installieren. Dabei wird die Datei, auf die die Secret-ID verweist, am gewünschten Speicherort eingebunden – in diesem Fall als lokale Datei .npmrc.

  • Die Datei .npmrc wird als Geheimnis eingebunden und niemals in das Docker-Image kopiert.

  • Vergessen Sie zuletzt nicht, die Datei .npmrc in die .dockerignore-Datei aufzunehmen, damit sie weder im Build- noch im Produktions-Image landet.

Sehen wir uns an, wie alles zusammenspielt. Zuerst die aktualisierte .dockerignore-Datei:

.dockerignore
node_modules
npm-debug.log
Dockerfile
.git
.gitignore
.npmrc

Dann die vollständige Dockerfile mit der aktualisierten RUN-Anweisung zur Installation von npm-Paketen und dem Einbindungspunkt für .npmrc:

__# --------------> 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 zum Erstellen des Node.js-Docker-Images:

$ docker build . -t nodejs-tutorial --secret id=npmrc,src=.npmrc

Hinweis: Secrets sind eine neue Docker-Funktion. Wenn Sie eine ältere Version verwenden, müssen Sie möglicherweise Buildkit wie folgt aktivieren:

$ DOCKER_BUILDKIT=1 docker build . -t nodejs-tutorial --build-arg NPM_TOKEN=1234 --secret id=npmrc,src=.npmrc

Zusammenfassung

Sie haben es geschafft und ein optimiertes Node.js-Docker-Basis-Image erstellt. Hervorragend!

Damit ist dieser Leitfaden zum Containerisieren von Node.js-Docker-Webanwendungen abgeschlossen. Dabei haben wir Leistungs- und Sicherheitsoptimierungen berücksichtigt, damit produktionsreife Node.js-Docker-Images entstehen!

Diese weiterführenden Ressourcen empfehle ich Ihnen besonders:

Nachdem Sie sichere und leistungsstarke Docker-Basis-Images für Ihre Node.js-Anwendungen erstellt haben, können Sie mit einem kostenlosen Snyk-Konto Schwachstellen in Ihren Containern finden und beheben.

Container-Sicherheit mit Fokus auf Entwickler

Snyk findet und behebt automatisch Schwachstellen in Container-Images und Kubernetes-Workloads.