Abhängigkeiten im Griff: Private Feeds für NuGet, npm, pip und Docker
Öffentliche Registries sind praktisch, bis sie es nicht mehr sind. Fällt ein Paket weg oder ist ein Registry‑Dienst vorübergehend nicht erreichbar, können Builds und Releases sofort blockiert werden. Abhängigkeiten sind daher nicht bloß Code‑Artefakte; ihre Verfügbarkeit, Lebenszyklen und Vertrauenswürdigkeit entscheiden über die Stabilität deiner Lieferkette.
In diesem Artikel geht es um ein pragmatisches Setup für Teams, die Web- und .NET-Workloads betreiben und regelmäßig mit NuGet, npm, pip und Docker/OCI (Open Container Initiative) arbeiten. Das Zielbild ist klar: reproduzierbare Builds, kontrollierte Supply Chain und weniger Zeitverlust in CI (Continuous Integration) durch langsame oder instabile Package Downloads.
Warum Teams eigene Feeds betreiben
Der offensichtlichste Grund ist Verfügbarkeit. Wenn dein Build seine Dependencies nicht bekommt, steht alles. Das ist ärgerlich in einer lokalen Entwicklerumgebung, aber es ist ein echtes Risiko für CI/CD (Continuous Integration/Continuous Delivery), wenn Releases nicht mehr durchlaufen. Das prominenteste Beispiel ist wohl der left-pad Vorfall auf npm, bei dem der Autor ein kleines, aber weit verbreitetes Package gelöscht hat, was Tausende von Builds weltweit zum Scheitern brachte. Aber auch andere Ökosysteme haben ähnliche Vorfälle erlebt, sei es durch unabsichtliches Löschen, böswillige Pakete oder einfach durch Ausfälle der Registry-Dienste.
Fast genauso häufig sind Limits. Docker Hub hat je nach Tier Pull Limits, die in CI erstaunlich schnell erreicht sind. Ein Blick in die Docker Hub Pull Usage und Limits reicht, um zu verstehen, warum ein Mirror schnell zur Pflicht wird.
Dazu kommen Lifecycle-Risiken: Packages können je nach Ökosystem entfernt, versteckt oder für neue Installationen blockiert werden. npm hat klare Regeln für Unpublishing im npm Registry. NuGet.org empfiehlt in vielen Fällen Unlisting statt Löschen. Python kennt das Konzept von yanked releases nach PEP 592, bei dem neue Installationen vermieden werden sollen, ohne bestehende, sauber gepinnte Builds zu brechen.
Ein eigener Feed ist außerdem ein Governance-Werkzeug. Du kannst kontrollieren, welche Versionen grundsätzlich genutzt werden dürfen, und du kannst Freigaben nachvollziehbar machen. Das ist wichtig für Auditability und Compliance, und es wird spätestens dann konkret, wenn du SBOMs (Software Bill of Materials) im Griff haben willst. Wenn du tiefer in das Thema einsteigen willst, schau in unseren Artikel: Governance: Software Bill of Materials (SBOM).
Begriffe, kurz sauber getrennt
unpublish: Paket(Version) ist nicht mehr verfügbar (z. B. npm).unlist: Paket bleibt verfügbar, taucht aber nicht mehr in der Suche auf (z. B. NuGet).yank: Paket bleibt verfügbar, soll aber nicht mehr neu installiert werden, wenn du nicht explizit pinnst (Python, PEP 592).deprecate: Paket ist weiterhin nutzbar, aber mit Warnhinweis.delete: endgültiges Entfernen. Das ist fast immer die härteste Option und selten die beste.
Betriebsmodelle: Proxy, Mirror, Curated Feed, Repo Manager
Es gibt nicht den einen richtigen Ansatz. In der Praxis kombinieren Teams oft mehrere Modelle und starten bewusst klein.
Ein Proxy bzw. Cache ist meist der Einstieg. Er holt Packages on demand aus der Public Registry und hält sie lokal vor. Das reduziert Bandbreite, beschleunigt Builds und macht dich resilienter gegen kurzzeitige Ausfälle. Azure DevOps Artifacts nennt das bei NuGet zum Beispiel Upstream Sources: Du nutzt einen einzigen Feed, und der Feed speichert verwendete Pakete aus dem Upstream.
Ein Mirror repliziert Inhalte, vollständig oder teilweise. Das ist sinnvoll in stark reglementierten Umgebungen oder wenn echte Offlinefähigkeit erforderlich ist.
Ein Mirror kann jedoch schnell teuer werden, wegen Storage, Egress und Betrieb. Außerdem kann es zu Inkonsistenzen kommen. Gespiegelte Artefakte können mit der Zeit von der öffentlichen Registry abweichen, fehlen oder veraltet sein. Prüfe auch Nutzungsbedingungen und Compliance, besonders wenn du große Teile einer öffentlichen Registry spiegeln möchtest.
Ein Curated Feed arbeitet nach festen Richtlinien: Nur genehmigte Pakete und Versionen werden aufgenommen. Das eignet sich für Teams mit erhöhten Compliance‑Anforderungen. In Azure Artifacts lässt sich das zum Beispiel über Views abbilden und durch einen Promotion-Workflow ergänzen. Dabei werden Pakete erst in einem Local Bereich veröffentlicht, durchlaufen dann Tests und Scans, und werden schließlich in den Release-Bereich promoted.
Ein Multi-Format Repository Manager bündelt alles in einem Tool: NuGet, npm, PyPI, Docker/OCI. Typische Kandidaten sind Sonatype Nexus Repository oder JFrog Artifactory. Der große Vorteil ist Single Source: Deine Build-Umgebungen kennen genau einen Endpoint, und das Tool entscheidet intern, ob etwas aus Cache, Mirror oder Curated Bereich kommt.
In der Praxis entscheiden klare Richtlinien und Konfigurationsoptionen:
Cache TTL: Wie lange bleiben Artefakte im Cache?Negative caching: Wie lange werden 404/Not Found-Ergebnisse zwischengespeichert, damit dein CI nicht ständig Anfragen ins Internet sendet?Retention: Was wird wann gelöscht, und wie stellst du sicher, dass Builds trotzdem reproduzierbar bleiben?Promotion: Pakete laufen von@Local, über@Prerelease, bis@Release.
NuGet & .NET: Upstreams, Views, Versioning, Restore-Stabilität
Für .NET-Teams ist der größte Hebel oft Restore-Determinismus. Ein Build ist nur dann reproduzierbar, wenn die Quellen stabil sind und du die Ergebnisse nicht dem Zufall überlässt. Dazu gehören drei Dinge: feste Package Sources, Lockfiles und ein CI, das Lockfiles wirklich respektiert.
Wenn du Azure Artifacts nutzt, sind Upstream Sources ein sehr pragmatischer Einstieg. Du richtest einen Feed ein, hängst NuGet.org als Upstream dahinter und verwendest im Build nur noch diesen einen Feed.
Wichtig sind auch saubere Berechtigungen. Die Feed Permissions sind granular genug, um zum Beispiel festzulegen, wer Packages speichern darf, wer publishen darf und wer promoten darf.
Ein typisches Single Source NuGet.Config sieht so aus:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="CompanyFeed" value="https://pkgs.dev.azure.com/ORG/PROJECT/_packaging/FEED/nuget/v3/index.json" />
</packageSources>
<!-- Optional: Absicherung, dass nuget.org nicht aus Versehen wieder genutzt wird -->
<disabledPackageSources>
<add key="nuget.org" value="true" />
</disabledPackageSources>
</configuration>
Aber auch in Pipeline-Tasks für den Restore kannst du den Feed oft explizit angeben. So stellst du sicher, dass nicht doch irgendwo eine andere Source genutzt wird. Dann sparst du dir eine NuGet.Config-Änderung, die manchmal zu Verwirrung führen kann, wenn Entwickler lokal andere Feeds nutzen.
Für CI lohnt es sich, Lockfiles zu aktivieren und Restore im Locked-Modus auszuführen. Das sorgt dafür, dass ein Restore nicht heimlich neu auflöst, nur weil irgendwo eine neue transitive Version verfügbar wurde.
<!-- Beispiel .csproj -->
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<RestoreLockedMode>true</RestoreLockedMode>
</PropertyGroup>
Beim Versionieren gilt ein einfaches Prinzip: Unveränderlichkeit. Eine veröffentlichte Version steht für einen festen Inhalt. Sie wird nicht überschrieben. Stattdessen erhöhst du die Versionsnummer (bump).
NuGet verwendet SemVer mit NuGet‑spezifischen Details, siehe die NuGet Package Versioning Doku. Zur Auffrischung empfiehlt sich die SemVer 2.0.0 Spezifikation.
Wenn ein Package gelöscht werden soll, ist unlist intern oft der bessere erste Schritt als delete. Das passt auch zu NuGet.org, wo Unlisting als Standardfall beschrieben wird. Ergänzend blockierst du die Version in deinem @Release View und kommunizierst aktiv, was zu tun ist.
npm: Private Registries, Scoped Packages, Unpublish vs Deprecate
Im npm-Ökosystem ist ein sauberer Namensraum Gold wert. Scopes wie @org/* machen sofort klar, was intern ist und was aus der Public Registry kommt. Wenn du GitHub Packages nutzt, erklärt die Doku zu GitHub Packages für npm die typischen Patterns für Scoped Registries.
Bei privaten Registries hast du mehrere Optionen wie Azure Artifacts, GitHub Packages oder Repo Manager. Wichtig ist, dass du die Registry-URL in deiner .npmrc zentral konfigurierst, damit alle Tools und Umgebungen den gleichen Endpoint nutzen.
Wichtig ist die Lifecycle-Semantik: unpublish ist oft das falsche Tool, weil es Builds bricht. Häufig besser ist deprecate, kombiniert mit klarer Kommunikation und einem Fix in einer höheren Version. Die npm-Regeln sind in der Unpublishing-Dokumentation gut beschrieben.
Für Release Channels sind dist-tags extrem praktisch. latest ist Production, next ist Preview. Damit kannst du Releases schrittweise ausrollen, ohne Versionen zu verstecken.
# Beispiel: Preview als next taggen
npm dist-tag add @lunaris/ui-kit@2.1.0-rc.1 next
# Später: Stable auf latest
npm dist-tag add @lunaris/ui-kit@2.1.0 latest
Für Reproduzierbarkeit ist npm ci (oder pnpm install --frozen-lockfile) der Standard. Lockfiles sind hier nicht optional, wenn du ernsthaft reproduzierbar sein willst.
Ein Single Source Setup erreichst du am einfachsten, indem dein Team nur einen Registry-Endpoint nutzt, der wiederum Public Packages proxied.
# .npmrc
registry=https://repo.example.com/repository/npm-group/
# Optional: wenn du doch Scopes separat routen musst
# @lunaris:registry=https://repo.example.com/repository/npm-internal/
always-auth=true
Wenn du stattdessen Public + Private direkt in der Tool-Konfiguration mischst, landest du schnell bei Edge Cases und im schlimmsten Fall bei Risiken wie Dependency Confusion. Ein zentrales Repo-Tool mit Proxy-Funktion nimmt dir diese Komplexität ab.
pip/Python: Indexes, Yanking, PEP 440, Mirrors mit Bedacht
Python ist in vielen Teams nicht der Haupt-Stack, aber es taucht überall auf: Build-Skripte, Tools, Security Scanner, Dokumentationspipelines.
Versioning in Python folgt PEP 440. Das ist wichtig, weil a/b/rc, .post und .dev nicht nur Syntax sind, sondern beeinflussen, ob Installer Pre-Releases ziehen oder nicht.
Wenn du einen eigenen Index betreibst, ist es hilfreich, die Spezifikation der PyPA Simple Repository API zu kennen. Für private Setups sind Tools wie devpi beliebt, Nexus/Artifactory können das ebenfalls.
Ein simples Setup für einen privaten Index:
# Beispiel: pip auf einen einzigen Index konfigurieren
pip config set global.index-url https://repo.example.com/repository/pypi/simple
# Optional: in CI zusätzlich trusted-host setzen, wenn TLS (Transport Layer Security) Inspection o. ä. im Spiel ist
# pip config set global.trusted-host repo.example.com
Für Reproduzierbarkeit reicht pinnen häufig nicht. Stark ist Hash-Pinning, weil du damit nicht nur die Version, sondern auch den Inhalt fixierst.
# requirements.txt (Ausschnitt)
requests==2.31.0 \
--hash=sha256:... \
--hash=sha256:...
Und wenn du gezielt steuern willst, welche transitive Versionen erlaubt sind, helfen Constraints:
# constraints.txt
urllib3<2
Yanking ist in Python ein wichtiges Werkzeug. PEP 592 beschreibt, wie Releases als yanked gekennzeichnet werden, damit Installer sie nicht mehr automatisch für neue Installationen auswählen. Mirrors müssen diese Metadaten korrekt übernehmen, sonst wird die beabsichtigte Semantik unterlaufen. Daher ist Mirror mit Bedacht keine Floskel, sondern eine betriebliche Notwendigkeit.
Docker/OCI: Registry Mirrors, Pull-Through Caches, Immutability
Bei Container‑Images gibt es zwei typische Probleme: Pull‑Limits und die Veränderlichkeit von Tags. Pull‑Limits sind bei Docker Hub gut dokumentiert, inklusive Motivation für Mirrors oder Caches, siehe Docker Hub Pull Usage.
Tags wie :latest oder :1.2 sind nicht unveränderlich, derselbe Tag kann heute einen anderen Inhalt referenzieren als gestern. Für reproduzierbare Deployments solltest du Images per Digest (sha256) referenzieren statt nur per Tag.
# Tag ist bequem, Digest ist reproduzierbar
FROM alpine@sha256:0123456789abcdef...
Für Caching gibt es zwei gängige Patterns:
- Docker-Daemon
registry-mirrorsfür Clients. - Ein Pull-through Cache nach dem Docker Registry Mirror Recipe.
Ein Beispiel für die Docker-Daemon-Konfiguration in /etc/docker/daemon.json:
{
"registry-mirrors": [
"https://mirror.example.com"
]
}
Ein Mirror ist keine vollständige Registry-Strategie. Für interne Images brauchst du Access Control, idealerweise Scanning und optional Signierung. OSS-Optionen sind z. B. Harbor oder die CNCF Registry Distribution. In Cloud-Stacks sind ACR/ECR/GAR oft der schnellste Weg.
Plane außerdem Cleanup, Retention und Garbage Collection von Anfang an ein. Storage wächst fast immer schneller, als man am Anfang denkt.
Security & Governance: Wer darf was, und was passiert bei CVEs?
Sobald du einen privaten Feed betreibst, betreibst du Infrastruktur. Und Infrastruktur braucht Rollen, Prozesse und klare Verantwortlichkeiten.
Startpunkt ist Zugriffskontrolle. Im NuGet-Umfeld zeigt Azure Artifacts mit seinem Rollenmodell sehr gut, was du trennen solltest: Lesen, publishen, promoten, löschen, und ganz wichtig, wer Packages aus Upstreams speichern darf. Details dazu stehen in den Azure Artifacts Feed Permissions.
Für GitHub Packages lohnt es sich, das Zusammenspiel aus Visibility und Berechtigungen zu verstehen, siehe Introduction to GitHub Packages.
Der zweite Block ist Approval. Ein einfaches, funktionierendes Muster ist Promotion: CI veröffentlicht nach @Local, Scans und Tests laufen, dann wird nach @Prerelease oder @Release promoted. Views (Azure Artifacts) sind dafür gemacht, siehe Azure Artifacts Views.
Vulnerability Management wird viel leichter, wenn du es an zwei Stellen machst: im CI und am Artefakt selbst. Als OSS-Scanner (Open Source) ist Trivy sehr verbreitet. Für Container kann auch Docker Scout ein sinnvoller Baustein sein.
Bei einer CVE (Common Vulnerabilities and Exposures) sollte für jedes Ökosystem ein klares Takedown-Verfahren definiert sein. Oft ist nicht mehr empfehlen (zum Beispiel deprecate, unlist, yank) der bessere erste Schritt als vollständiges Löschen. Letzteres kann reproduzierbare, ältere Builds brechen.
| Aktion | NuGet | npm | Python/pip | Docker/OCI |
|---|---|---|---|---|
| Nicht mehr empfehlen | unlist | deprecate | yank | Tag nicht mehr nutzen, Digest blocken |
| Harte Sperre in Prod | @Release blocken | Promotion stoppen | Private Index Policy | Registry Policy / Admission |
| Vollständig entfernen | delete (selten) | unpublish (selten) | delete (selten) | delete/GC (mit Vorsicht) |
Eine minimale production-ready Checkliste für Artifact-Infrastruktur:
- Ein Endpoint pro Ökosystem (besser: ein Tool für alle)
- RBAC (Role Based Access Control) mit getrennten Rollen für Read/Publish/Promote/Delete
- Lockfiles und frozen/locked Install-Modi in CI
- Retention-/Backup-Strategie plus Monitoring
- Scanning (SBOM + CVE) und dokumentierter Incident-Ablauf
Tool-Auswahl & Startplan: klein anfangen, sauber skalieren
Die Wahl eines Tools sollte nicht von persönlichen Vorlieben abhängen, sondern von den Anforderungen: unterstützte Formate, Proxy/Cache‑Fähigkeiten, RBAC und Audit‑Fähigkeiten, Promotion/Release‑Gating, Hochverfügbarkeit/Backup, Kosten sowie SaaS (Software as a Service) vs. Self-hosted.
Damit du die Kandidaten nicht an fünf Stellen im Kopf zusammensetzen musst, hier eine zentrale Tool‑Matrix. Sie ist bewusst pragmatisch: Fokus auf die im Artikel genannten Ökosysteme (NuGet, npm, PyPI/pip, Docker/OCI) und die typischen Enterprise‑Anforderungen.
Hinweis: Features hängen bei vielen Produkten an Editionen/Add-ons. Scanning/Gating ist häufig ein separates Produkt (z. B. JFrog Xray, Sonatype Repository Firewall/IQ).
Kostenpflichtige Tools
| Tool | Formate | Kernfeatures |
|---|---|---|
| Azure DevOps Artifacts | NuGet, npm, Maven, PyPI, Cargo | Integriert in Azure DevOps; Upstreams/Views; feingranulare Feed‑Permissions |
| GitHub Packages | npm, Maven, NuGet, Ruby, Docker | GitHub‑native, gute Integration mit Actions; RBAC über Repo/Org |
| JFrog Artifactory | NuGet, npm, PyPI, Docker, u. a. | Enterprise‑Multi‑format; Promotion/Replication; Scanning via Xray |
| Sonatype Nexus Repository | NuGet, npm, PyPI, Docker | OSS + Enterprise‑Ecosystem; Repo‑Groups/Proxy; Firewall/IQ für Scans |
Kostenlose / Open Source Tools
| Tool | Formate | Kernfeatures |
|---|---|---|
| Harbor | Docker/OCI, Helm | Kubernetes‑native OCI Registry; Replikation; Trivy/Clair Integration |
| Docker Distribution | Docker/OCI | Minimal Registry / Pull‑through Mirror |
| Verdaccio | npm | Lightweight npm‑Cache/Proxy; Plugin‑Ökosystem |
| devpi | PyPI | Einfacher privater PyPI‑Index mit Caching und Auth |
Hinweis: Scan/Gating ist häufig ein separates Add‑on oder läuft in CI; Feature‑Verfügbarkeit variiert nach Edition/Lizenz.
Pragmatischer Startplan
Wer einen privaten Feed aufbauen will, sollte nicht versuchen, alles auf einmal zu machen. Stattdessen empfehle ich einen iterativen Ansatz:
-
Phase 1: Cache/Proxy + Lockfiles
Richte einen einzelnen, als Single‑Source genutzten Endpoint ein und betreibe CI im frozen/locked‑Modus. So stellst du sofort Reproduzierbarkeit und Resilienz sicher. -
Phase 2: Promotion, Views & Scanning
Ergänze Promotion-Workflows (z. B. Local → Prerelease → Release), automatisierte Scans und RBAC, damit nur geprüfte Artefakte in Produktion gelangen. -
Phase 3: Curated Allowlist, Ownership & Runbook
Definiere eine Allowlist für produktive Pakete, kläre Verantwortlichkeiten und dokumentiere Incident‑/CVE‑Abläufe inklusive Retention, Backup und Recovery.
Dabei sollten folgende Risiken und Herausforderungen im Blick behalten und klar benannt werden:
- Laufende Betriebskosten (Compute, Storage, Netzwerk)
- Storage‑Wachstum und damit verbundene Cleanup‑Aufwände
- Gefahr veralteter oder inkonsistenter Mirrors
- Nutzungsbedingungen/Compliance‑Risiken beim Spiegeln öffentlicher Registries
- Single‑Point‑of‑Failure bei mangelnder Hochverfügbarkeit
Private Feeds sind Infrastruktur, kein Spielzeug. Wenn du nur einen Schritt machst, mache ihn reproduzierbar. Ein Single‑Source‑Endpoint plus Lockfiles im frozen/locked-Modus beseitigen die meisten Alltagsprobleme.
Wie betreibst du private Feeds und Registry Mirrors in deinen Projekten? Schau gerne bei uns auf LinkedIn vorbei und diskutiere mit.