„Container-Image“ ist im Sprachgebrauch fast zum Synonym für „Docker“ geworden — dabei ist das Format längst von Docker entkoppelt und in der Open Container Initiative
(OCI) standardisiert. Dieser Beitrag erklärt, was das konkret bringt, warum meine Build-Pipeline deshalb mit Skopeo und Crane statt docker push arbeitet, und wie ich meine bisherige Harbor-Registry gegen das deutlich schlankere
Zot
getauscht habe.
Was OCI eigentlich ist#
Hinter dem Kürzel stecken drei aufeinander aufbauende Spezifikationen, die zusammen das gesamte Lebenszyklus-Vokabular einer Registry definieren:
- Die Image Spec
beschreibt, wie ein Image aufgebaut ist — Layer, Config, und ein Manifest, das beides über ihre
sha256-Digests zusammenbindet. Alles ist content-addressable: Der Digest ist der Inhalt. - Die Distribution Spec definiert die HTTP-API, über die diese Blobs und Manifeste geschoben und gezogen werden. Jede konforme Registry spricht dieselbe Sprache.
- Die Artifact- und Referrers-Erweiterungen erlauben schließlich, beliebige Inhalte neben einem Image abzulegen und zu verknüpfen — Helm-Charts, SBOMs, WASM-Module und, für mich besonders relevant, cosign -Signaturen.
Der praktische Gewinn dieser Standardisierung ist Werkzeugfreiheit. Weil das Format und die API offen sind, ist niemand mehr an einen bestimmten Daemon oder Anbieter gebunden — eine Eigenschaft, die meine gesamte Build- und Registry-Architektur trägt.
Warum nicht mehr Harbor#
Harbor
ist eine ausgewachsene Enterprise-Registry — und genau das wurde ihm bei mir zum Verhängnis. Eine Harbor-Installation ist kein Dienst, sondern ein Verbund: core, portal, jobservice, eine eingebettete Registry, dazu PostgreSQL und ein Redis-kompatibler Cache, optional Notary und Trivy als eigene Komponenten. Das ist viel beweglicher Teil für „ich möchte meine selbstgebauten Images ablegen“.
Dazu kam ein handfester, sehr konkreter Blocker: Mein Cluster läuft auf ARM64-Hardware , und für genau diese Architektur fehlte Harbor zum damaligen Zeitpunkt offizieller Support — der entsprechende Merge stand noch aus. Eine Enterprise-Registry, die auf meiner Plattform gar nicht erst sauber startet, ist die denkbar schlechteste Wahl.
Zot: nur OCI, dafür ganz#
Zot ist das genaue Gegenteil: ein einzelner Go-Prozess, der ausschließlich die OCI-Spezifikationen implementiert — keine proprietären Erweiterungen, kein Datenbank-Cluster, sauberer ARM64-Support. Die Konfiguration ist eine einzige JSON-Datei, und schon die Storage-Sektion zeigt, dass man es hier mit einer ernstzunehmenden Registry zu tun hat: Garbage Collection, ein periodischer Integritäts-scrub und eine deklarative Retention-Policy:
1"storage": {
2 "gc": true,
3 "gcInterval": "6h",
4 "retention": {
5 "policies": [{
6 "repositories": ["**"],
7 "deleteUntagged": true,
8 "keepTags": [{ "mostRecentlyPushedCount": 10, "pushedWithin": "720h" }]
9 }]
10 }
11}
Erreichbar ist die Registry unter oci.this-is-fine.io, eingebunden über das Envoy-Gateway
wie jeder andere Dienst.
Der Fallstrick: kein docker push#
Die größte Umstellung gegenüber dem gewohnten Workflow: In meiner Build-Pipeline
kommt kein docker push zum Einsatz, sondern
Skopeo
und
Crane
. Das hat handfeste Gründe:
Daemonlos.
docker pushsetzt einen laufenden Docker-Daemon voraus. Skopeo und Crane sprechen die OCI-Distribution-API direkt über HTTP — kein Daemon, kein Socket, keine Root-Rechte auf dem CI-Runner. Das passt zu einem flüchtigen, zustandslosen Build-Agenten.Multi-Arch ohne Buildx-Krücke. Ich baue
amd64undarm64getrennt und füge sie erst nachgelagert zu einem Multi-Arch-Index zusammen. Das ist exakt Cranes Spezialgebiet —crane index appendbaut das Index-Manifest aus den einzelnen Architektur-Manifesten, ohne dass ein Daemon je beide gleichzeitig sehen muss:1skopeo copy "docker-archive:img-arm64.tar" "docker://oci.this-is-fine.io/${img}:build-arm64" 2crane index append --manifest …:build-amd64 --manifest …:build-arm64 --tag …:${TAG}Der Lohn zeigt sich erst beim Pull: Zieht ein ARM64-Knoten versehentlich ein reines
amd64-Image, quittiert er das mit einemexec format error— kein Registry-, sondern ein Build-Fehler, den ein sauberer Multi-Arch-Index gar nicht erst entstehen lässt.OCI-Artefakte statt nur Images. Die cosign-Signaturen, die meine Images absichern, sind selbst OCI-Artefakte, die über die Referrers-API am Image hängen. Skopeo und Crane behandeln diese Manifest-Typen nativ; der klassische Docker-Workflow hat dafür kein sauberes Vokabular.
Kurz: Sobald man die Registry als das begreift, was sie ist — ein content-addressable OCI-Store, nicht ein „Docker-Ding“ —, sind daemonlose, OCI-native Werkzeuge die natürliche Wahl.
Zugriff: Menschen per OIDC, Maschinen per htpasswd#
Authentifizierung trenne ich strikt nach Akteur. Menschen melden sich am Web-UI über OIDC bei Pocket-ID
an; Maschinen nutzen statische htpasswd-Credentials. Die Autorisierung regelt Zots accessControl über Gruppen:
1"accessControl": {
2 "groups": {
3 "admins": { "users": ["admin", "buildbot", "clawbot"] },
4 "users": { "users": ["fetchbot"] }
5 },
6 "repositories": { "**": { "policies": [{ "groups": ["users"], "actions": ["read"] }], "defaultPolicy": [] } },
7 "adminPolicy": { "groups": ["admins"], "actions": ["read", "create", "update", "delete"] }
8}
Das Prinzip dahinter ist „least privilege“ in Reinform:
Schreibrechte hat einzig die Build-Pipeline (buildbot); der Pull-Bot des Clusters (fetchbot) darf ausschließlich lesen, und anonyme Zugriffe laufen gegen eine leere defaultPolicy ins Leere. Wer Images baut, und wer sie nur konsumiert, ist damit kryptografisch wie autorisatorisch sauber getrennt.
Eingebaute Sicherheit: Trivy und cosign#
Zwei Erweiterungen runden das Bild ab. Die Suche bindet Trivy
ein und scannt die abgelegten Images täglich gegen die CVE-Datenbank — ich sehe also direkt im UI, ob ein Image bekannte Schwachstellen mitschleppt. Und die trust-Erweiterung verifiziert cosign-Signaturen, sodass die signierten Images aus meiner Pipeline
auch in der Registry-Oberfläche als vertrauenswürdig ausgewiesen werden:
1"extensions": {
2 "search": { "cve": { "updateInterval": "24h", "trivy": { … } } },
3 "trust": { "enable": true, "cosign": true },
4 "scrub": { "enable": true, "interval": "24h" }
5}
Fazit#
Der Tausch von Harbor gegen Zot ist dasselbe Muster, das sich durch mein ganzes Lab zieht: das spezialisierte, schlanke Werkzeug schlägt die eierlegende Wollmilchsau, sobald man deren Features nicht wirklich braucht. Geblieben ist eine Registry, die genau die OCI-Spezifikationen spricht, sauber auf ARM64 läuft, Signaturen und Schwachstellen kennt — und sich mit Skopeo und Crane so anfühlt, wie das Format eigentlich gemeint war.
Nachtrag (25. Juni 2026): Harbor ist endgültig weg#
Lange stand Harbor noch auskommentiert in der Overlay-Kustomization — mit einem FIXME: Still no ARM64 support, waiting for merge daneben, als Hintertür für den Fall, dass Zot mir doch etwas Wichtiges schuldig bleibt. Diese Hintertür habe ich jetzt zugemacht: Der komplette Harbor-Baum ist gelöscht — das HelmRelease, die eigene CloudNativePG-Datenbank, das Dragonfly-Redis, die VolSync-Backups, die Secrets, der Namespace und die HelmRepository-Source. Zot hat in einem halben Jahr Betrieb nichts vermissen lassen, was den Wechsel rückgängig machen würde. Die Migration ist damit nicht nur vollzogen, sondern auch wirklich abgeschlossen.
