Optimierungen am Dockerfile für NPM-Projekte

docker

​Bestimmt nutzt ihr Jenkins, Bamboo, GitLab oder andere Tools, um eure NPM-basierten Projekte automatisiert zu testen und zu bauen. Dabei müssen immer Voraussetzungen erfüllt sein, um den Build-Prozess durchführen zu können. Zum Beispiel wollt ihr eine bestimmte Node.js-Version zum Bauen der Anwendung einsetzen. Oder ihr müsst einen Proxy konfigurieren, um eure NPM-Abhängigkeiten aus dem Unternehmens-Repository zu laden. Wie man ein Nexus für NPM-Pakete vorbereitet, habe ich in einem anderen Post beschrieben.

Eine häufige Lösung ist, den Build-Prozess durch ein Dockerfile zu beschreiben. Das Dockerfile liegt direkt beim Quellcode und wird beim Start des Build-Prozesses von den oben genannten Tools ausgeführt. Nachfolgend möchte ich euch zeigen, wie ein Dockerfile für NPM-Projekte aussehen kann. Dabei möchte ich auf ein paar Aspekte genauer hinweisen, um den Build-Prozess möglichst schnell und performant zu gestalten. Das fertige Beispiel ist wie immer auf GitHub zu finden.

Der Build-Prozess

Als Beispielprojekt dient uns ein einfaches Angular Projekt. Für unser Projekt legen wir uns ein Dockerfile an. In dieser Datei beschreiben wir gleich den (vereinfachten) Build-Vorgang. 

FROM node:12.16.1-buster-slim 
Die erste Zeile des Dockerfiles gibt das Basis-Image an. Auf diesem Image baut also unser Build-Prozess auf. Gefunden habe ich dieses offizielle Image auf Docker Hub. Eine Node.Js 12.16.1 Installation ist, wie der Tag des Images andeutet, schon installiert. Auf das buster-slim im Tag-Namen des Images möchte ich in diesen Blog-Post nicht weiter eingehen. Nur soviel: es geht darum, welche anderen Konfigurationen und Installationen ebenfalls in dem Docker-Image installiert sind.
Mit der ADD-Angabe können wir etwas zum Image hinzufügen. Natürlich wollen wir als nächstes den Quellcode hinzufügen, um den Build-Prozess durchführen zu können. Häufig sieht das wie folgt aus.
FROM node:12.16.1-buster-slim

ADD . /app

RUN npm install 
Der gesamte Quellcode des Repositories wurde in das /app Verzeichnis kopiert. Das Docker-Image wird mit dem nächsten Befehl folgendermaßen gebaut:

docker build -t angular-build-via-docker . 
Das -t angular-build-via-docker  ist der Tag-Name unseres Images. Wir bauen also auf dem node Image auf und führen unseren Build-Prozess durch und nennen das fertige Image angular-build-via-docker. Der Punkt am Ende des Befehls gibt den Ort des Dockerfiles an.

Führen wir diesen Befehl nun aus, so wird als erstes das node:12.16.1-buster-slim  Image geladen. Das Image wird von den Node.Js Entwicklern selbst bereitgestellt. Danach wird durch die ADD Angabe der Inhalt unseres Repositories zum Container hinzugefügt. Das kann man auch an der folgenden Ausgabe nachvollziehen.

Dockerignore

Sending build context to Docker daemon  406.1MB 

 Über 400 MB werden zum Container hinzugefügt? Kann das sein? Anscheinend schon. Denn unter anderem wird der Inhalt des node_modules/ und .git/ Verzeichnisse hinzugefügt. Gerade das node_modules/ Verzeichnis brauchen wir nicht für den Build-Prozess. Das Beziehen der Abhängigkeiten soll während des Bauens des Docker Images passieren.

Was dem Image hinzugefügt wird, können wir durch die Datei .dockerignore steuern. Die Datei funktioniert nach dem gleichen Prinzip wie die .gitignore.  Die beiden aufgeführten Ordner werden somit nicht mehr dem Image hinzugefügt. Nachfolgend der Inhalt unserer .dockerignore.

node_modules/
.git/
 

Der Docker Cache

Wird jetzt der docker build Befehl ausgeführt, werden dem Image nur noch etwa 500 KB hinzugefügt. Danach folgt der npm install Befehl. Für jeden neuen Build des Images müssen die Abhängigkeiten neu heruntergeladen werden. Dieses Vorgehen verlangsamt den Prozess stark! Aber auch hier gibt es einen Trick. Nachfolgend das fertige Dockerfile.
FROM node:12.16.1-buster-slim

ADD package.json /app/
WORKDIR /app
RUN npm install

ADD . /app

RUN npm run build
 
Der Trick ist folgender: Es wird nicht der komplette Quellcode hinzugefügt, sondern erstmal nur die package.json. Diese Datei beinhaltet alle Informationen, um die Abhängigkeiten zu laden. Die Docker-Engine kann die einzelne Datei darauf überprüfen, ob diese sich von vorherigen docker build  Ergebnissen unterscheidet. Wird keine Veränderung festgestellt, kann der Build-Prozess auf einem vorherigen Ergebnis eines Builds aufbauen. Sprich, wenn ein äquivalenter npm install durchgeführt wurde, können nachfolgende (oder andere) Docker-Images darauf aufbauen. Wir profitieren also von einer erheblichen Zeitersparnis, die wir nicht hätten, wenn wir gleich den gesamten Quellcode hinzufügen würden. Die Erkennung, ob sich etwas an den hinzugefügten Dateien geändert hat, funktioniert nur bei einzeln hinzugefügten Dateien wie der package.json und nicht bei einer großen Menge an Dateien.

Nachfolgend fehlt nur noch der Befehl, um den Build des Angular Projektes auszuführen. Das ist in der letzten Zeile im Dockerfile zu finden.​

Ausprobieren kann man das nun ganz einfach. Nachdem der docker build Befehl fertig ist, einfach nochmal ausführen. Dann sieht man folgende Zeile:

Step 2/6 : ADD package.json /app/
 ---> Using cache 

Man erkennt sofort, dass der zweite Schritt nun viel schneller erledigt ist.

Das Ende vom Lied

​Wir haben durch zwei einfache Anpassungen erreicht, dass unser Build-Prozess effizienter und schneller durchgeführt werden kann.

Durch die .dockerignore Datei können wir einfach steuern, welche Inhalte dem Image hinzugefügt werden. Außerdem können wir den Caching-Mechanismus ausnutzen und Teile des Build-Prozesses beschleunigen, indem sie nur ausgeführt werden, wenn zwingend erforderlich.

Ausblick 

Natürlich könnt ihr die gleichen Tricks für andere Projekte und Programmiersprachen anwenden. Auf Apache Maven basierte Projekte sollte man z.B. nur die pom.xml zum Image hinzufügen und dann die Abhängigkeiten laden. Erst danach den Quellcode hinzufügen und bauen ;-)

By accepting you will be accessing a service provided by a third-party external to https://blog.ordix.de/