Ein Build funktioniert auf dem eigenen Rechner, aber in der CI-Pipeline fehlen plötzlich Pakete oder Versionen? Genau hier sorgt npm ci für reproduzierbare Installationen, weil Abhängigkeiten strikt aus der Lockdatei installiert werden. Ich zeige, wie der Befehl arbeitet, wann er gegenüber npm install die bessere Wahl ist und welche Fehler in Node.js-Projekten besonders häufig auftreten.
Die wichtigsten Fakten zur reproduzierbaren Installation
-
Lockdatei erforderlich: Eine passende
package-lock.jsonodernpm-shrinkwrap.jsonmuss vorhanden sein. -
Sauberer Zustand: Ein vorhandener
node_modules-Ordner wird vor der Installation entfernt. -
Keine Änderungen: Die Lockdatei und
package.jsonwerden nicht automatisch aktualisiert. - Ideal für CI/CD: Tests, Deployments und Container-Builds erhalten jedes Mal denselben Abhängigkeitsbaum.
Was der Clean-Install-Befehl tatsächlich macht
Der Befehl installiert die in der Lockdatei festgelegten Pakete in einem frischen Projektzustand. Anders als bei einer normalen Installation versucht npm nicht, Versionskonflikte nebenbei zu lösen oder die Lockdatei anzupassen. Entspricht die Auflösung in package-lock.json nicht den Angaben aus package.json, bricht der Vorgang mit einem Fehler ab.
Vor dem eigentlichen Installieren löscht npm einen vorhandenen node_modules-Ordner. Das ist praktisch, weil alte oder manuell veränderte Pakete nicht unbemerkt in den Build einfließen. Gleichzeitig sollte man den Befehl lokal nicht gedankenlos im Projektverzeichnis ausführen, wenn dort noch nicht gespeicherte Anpassungen in node_modules liegen.
npm ciDie Installation gilt als eingefroren. Weder Abhängigkeiten werden ergänzt noch Versionsbereiche neu ausgewertet. Genau diese Strenge ist der Grund, warum ich den Befehl für automatisierte Abläufe bevorzuge.
npm install und der saubere Installationslauf im Vergleich
Beide Befehle installieren Abhängigkeiten, verfolgen aber unterschiedliche Ziele. npm install eignet sich vor allem für die Entwicklung, wenn Pakete hinzugefügt, aktualisiert oder die Lockdatei verändert werden soll. Der Clean-Install-Modus ist dagegen für einen bereits festgelegten Stand gedacht.
| Merkmal | npm install |
Clean-Install-Modus |
|---|---|---|
| Lockdatei erforderlich | Nein | Ja |
| Abhängigkeiten aktualisieren | Kann passieren | Nein |
Vorhandenes node_modules
|
Wird meist weiterverwendet | Wird entfernt |
| Einzelnes Paket hinzufügen | Ja | Nein |
| Typischer Einsatz | Lokale Entwicklung | CI/CD, Tests und Deployment |
Ein häufiger Denkfehler besteht darin, den strengeren Befehl als grundsätzlich bessere Variante zu betrachten. Für die tägliche Entwicklung wäre das oft unbequem, weil jede Änderung an einer Abhängigkeit zuerst über npm install und die Lockdatei vorbereitet werden muss. Für einen Build ist genau diese Einschränkung jedoch ein Vorteil, denn unerwartete Versionsänderungen werden sichtbar, statt stillschweigend akzeptiert zu werden.
So setze ich den Befehl in CI/CD und Containern ein
Im automatisierten Workflow sollte die Lockdatei Bestandteil des Repositorys sein. Ein typischer Ablauf besteht aus dem Auschecken des Codes, der Installation der Abhängigkeiten, den Tests und anschließendem Build.
npm ci
npm test
npm run buildEntscheidend ist, dass die Umgebung eine passende Node.js- und npm-Version verwendet. Unterschiedliche npm-Versionen können bei Peer-Abhängigkeiten oder Lockdateiformaten zu Abweichungen führen. Ich halte deshalb die Runtime im CI-System, im Dockerfile und in der lokalen Entwicklungsumgebung möglichst konsistent.
Beispiel für einen Docker-Build
FROM node:22
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run buildDas Kopieren der beiden Manifestdateien vor dem restlichen Quellcode verbessert den Docker-Cache. Ändert sich nur der Anwendungscode, muss die Abhängigkeitsinstallation nicht bei jedem Build erneut ausgeführt werden. Für ein Produktions-Image kann zusätzlich die Option --omit=dev sinnvoll sein, wenn Entwicklungsabhängigkeiten dort nicht benötigt werden.
npm ci --omit=devDiese Variante ist allerdings nur dann richtig, wenn der anschließende Prozess ohne Test- und Build-Werkzeuge auskommt. Werden beispielsweise TypeScript, bundler oder Testframeworks während des Builds benötigt, sollten die Entwicklungsabhängigkeiten erst nach dem Build entfernt oder in einer mehrstufigen Docker-Datei getrennt behandelt werden.
Typische Fehler und ihre zuverlässige Lösung
Die Lockdatei fehlt
Ohne package-lock.json kann npm keinen exakt festgelegten Abhängigkeitsbaum installieren. In diesem Fall führe ich einmal lokal npm install aus, prüfe das Ergebnis und committe die erzeugte Lockdatei. Sie gehört normalerweise ins Repository, während node_modules dort nichts zu suchen hat.
package.json und Lockdatei passen nicht zusammen
Wurde eine Abhängigkeit manuell in package.json ergänzt, aber die Lockdatei nicht aktualisiert, stoppt der Build. Die saubere Lösung ist eine bewusste lokale Aktualisierung mit npm install, gefolgt von einer Prüfung der Änderungen. Die Lockdatei einfach zu löschen, verschleiert das Problem und kann zu neuen, nicht getesteten Versionen führen.
Peer-Abhängigkeiten verursachen einen Konflikt
Besonders bei React-, TypeScript- oder Build-Toolchains treten Konflikte zwischen erwarteten Paketversionen auf. Optionen wie --legacy-peer-deps können in bestimmten Projekten nötig sein, sollten aber nicht zufällig nur im CI-Lauf verwendet werden. Wurde die Lockdatei mit einer solchen Option erzeugt, muss dieselbe Einstellung auch beim späteren Clean Install aktiv sein.
Ich lege solche projektweiten Einstellungen lieber in einer versionierten .npmrc ab. Dadurch verwendet jedes Teammitglied und jeder Build dieselben Installationsregeln, statt sich auf individuelle Shell-Konfigurationen zu verlassen.
Installationsskripte schlagen fehl
Einige Pakete kompilieren native Bestandteile oder führen beim Installieren eigene Skripte aus. Fehler bei Python, Compiler-Werkzeugen oder Systembibliotheken liegen dann nicht unbedingt an npm selbst. In CI-Images müssen die benötigten Build-Werkzeuge vorhanden sein, bevor die Abhängigkeiten installiert werden.
Die Option --ignore-scripts kann das Risiko unerwünschter Installationsskripte reduzieren, verhindert aber auch legitime Schritte wie den Aufbau nativer Module. Ich setze sie deshalb nur ein, wenn ich genau weiß, welche Pakete betroffen sind.
Welche Einstellungen für Sicherheit und Teams sinnvoll sind
Ein reproduzierbarer Installationslauf erhöht die Stabilität, ersetzt aber keine Sicherheitsprüfung. Die Lockdatei fixiert Versionen, macht ein Paket dadurch jedoch nicht automatisch sicher. Für regelmäßige Kontrollen gehören Dependency-Updates, Code-Reviews und npm-Audit-Prüfungen weiterhin in den Entwicklungsprozess.
Private Pakete benötigen im CI-System eine sichere Authentifizierung. Zugangstoken gehören nicht in das Repository, sondern in die Geheimnisverwaltung der jeweiligen Plattform. Eine projektweite .npmrc sollte nur auf eine Umgebungsvariable verweisen, deren Wert der Build-Dienst zur Laufzeit bereitstellt.
In Monorepos mit npm Workspaces kann der Clean Install den gesamten Arbeitsbereich installieren. Selektive Workspace-Optionen sind möglich, bringen aber zusätzliche Komplexität in Cache- und Build-Logik. Für kleine Projekte ist ein vollständiger Installationslauf meist transparenter, während große Monorepos von gezielten Workflows profitieren können.
Der wichtigste Teamstandard ist aus meiner Sicht schlicht. Änderungen an Abhängigkeiten werden lokal bewusst erzeugt, die Lockdatei wird mit geprüft und CI installiert anschließend exakt diesen Stand. So bleibt nachvollziehbar, welche Änderung den Build beeinflusst.
Ein verlässlicher Ablauf für jedes Node.js-Projekt
Für die lokale Entwicklung nutze ich den normalen Installationsbefehl, wenn Pakete ergänzt oder aktualisiert werden. Sobald der gewünschte Stand feststeht, committe ich package.json und die Lockdatei gemeinsam und lasse automatisierte Umgebungen ausschließlich den eingefrorenen Installationslauf verwenden.
Schlägt er fehl, ist das zunächst eine nützliche Information. Meist weist der Fehler auf eine fehlende Lockdatei, eine nicht synchronisierte Abhängigkeit, inkompatible Peer-Anforderungen oder eine unvollständige Build-Umgebung hin. Wer diese Ursachen gezielt prüft, erhält reproduzierbare Builds ohne die unangenehme Überraschung, dass lokal und im Deployment unterschiedliche Pakete landen.