Image Volumes mounten keine OCI-Artefakte
04.08.2026
Thema: alle Beiträge zu Kubernetes
Kubernetes kann Images als Volume mounten. Echte OCI-Artefakte aber nicht, und containerd sagt es dir nicht. Der Pod läuft, das Verzeichnis bleibt leer.
Inhaltsverzeichnis
- Was Image Volumes sind
- Der Versuchsaufbau
- Der stumme Fehlschlag
- Warum der Mount leer bleibt
- Die Gegenprobe
- Das Beispiel aus der Dokumentation ist kein Artefakt
- Wer kann es, wer nicht
- Was daraus folgt
- Fazit
Ein Pod, der sauber läuft. Ein Volume, das gemountet ist. Keine Fehlermeldung, kein Warning-Event, ein grüner Status. Und ein leeres Verzeichnis.
Im letzten Beitrag habe ich ein PDF als OCI-Artefakt in eine Container-Registry gepusht, um zu zeigen, dass eine Registry keine Images kennt, sondern Manifeste und Blobs. Die naheliegende Anschlussfrage kam prompt: Kubernetes kann seit Version 1.31 Images als Volume in einen Pod mounten. Wenn eine Registry beliebige Dateien speichert und Kubernetes aus Registry-Inhalten Volumes macht, dann müsste sich dieses PDF doch direkt in einen Container mounten lassen. Ohne Init-Container, ohne ConfigMap, ohne Sidecar, der etwas herunterlädt.
Die Antwort ist nein. Interessant ist nicht das Nein, sondern wie es sich äußert.
Was Image Volumes sind
Der Volume-Typ image kam mit Kubernetes 1.31 als Alpha, wurde in 1.33 Beta und ist seit 1.35 standardmäßig eingeschaltet. Er sieht so aus:
volumes:
- name: pdf
image:
reference: registry.example.com/spickzettel:v1
pullPolicy: Always
Das Prinzip ist bestechend einfach. Der Inhalt einer Referenz aus einer Registry wird read-only in den Container gemountet. Damit trennt man Binärdateien von Konfiguration und Daten, ohne dafür ein eigenes Auslieferungsverfahren zu bauen. Typische Anwendungen sind statische Assets, Regelwerke, Testdaten oder KI-Modelle.
Die Dokumentation formuliert es weit: „This allows you to mount content from OCI registries inside containers.” Und kubectl describe beschreibt den Volume-Typ wörtlich als „Image (a container image or OCI artifact)”. Beides klingt so, als sei der Fall damit erledigt.
Der Versuchsaufbau
Nachbauen lässt sich das in zehn Minuten mit k3d. Zuerst eine Registry und ein Cluster, das sie kennt:
k3d registry create artefakte --port 5111
k3d cluster create artefakt-test --registry-use k3d-artefakte:5111 --agents 0
Der Cluster, gegen den ich gemessen habe:
$ kubectl get nodes -o wide
NAME STATUS VERSION CONTAINER-RUNTIME
k3d-artefakt-test-server-0 Ready v1.35.5+k3s1 containerd://2.2.3-k3s1
Kubernetes 1.35 mit containerd 2.2.3, also weit jenseits der von der Dokumentation geforderten Mindestversion containerd 2.1. Nun dasselbe Artefakt wie im letzten Beitrag, ein PDF mit frei erfundenem artifactType:
oras push --plain-http localhost:5111/spickzettel:v1 \
--artifact-type application/vnd.trutz.spickzettel \
spickzettel.pdf:application/pdf
Das Manifest, das dabei entsteht, ist ein lehrbuchmäßiges OCI-Artefakt:
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.trutz.spickzettel",
"config": {
"mediaType": "application/vnd.oci.empty.v1+json",
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"size": 2,
"data": "e30="
},
"layers": [
{
"mediaType": "application/pdf",
"digest": "sha256:2363a17e1a2ef9030b56ffba087a7ab9401f9043aadce443f2694c7f64663084",
"size": 32553,
"annotations": {
"org.opencontainers.image.title": "spickzettel.pdf"
}
}
]
}
Leere Config, eigener artifactType, ein Layer mit dem Medientyp der Nutzlast. Genau das, was die OCI-Spezifikation unter „Guidelines for Artifact Usage” beschreibt.
Dazu ein Pod, der nichts weiter tut, als hineinzuschauen:
apiVersion: v1
kind: Pod
metadata:
name: spickzettel-artefakt
spec:
containers:
- name: leser
image: busybox:1.37
command: ["sh", "-c", "ls -lR /spickzettel; sleep 3600"]
volumeMounts:
- name: pdf
mountPath: /spickzettel
volumes:
- name: pdf
image:
reference: k3d-artefakte:5111/spickzettel:v1
pullPolicy: Always
Der stumme Fehlschlag
$ kubectl get pod spickzettel-artefakt
NAME READY STATUS RESTARTS AGE
spickzettel-artefakt 1/1 Running 0 15s
Der Pod läuft. Die Events sind makellos:
Normal Pulled 14s kubelet Successfully pulled image
"k3d-artefakte:5111/spickzettel:v1" in 119ms.
Image size: 33127 bytes.
Normal Started 11s kubelet spec.containers{leser}: Container started
Das Artefakt wurde gefunden, geladen, die Größe stimmt auf das Byte. Kein Warning, kein Fehler, nichts im Pod-Status. Nur der Blick hinein verrät es:
$ kubectl exec spickzettel-artefakt -- sh -c 'ls -la /spickzettel; mount | grep spickzettel'
total 8
drwxr-xr-x 2 root root 4096 Aug 2 07:39 .
drwxr-xr-x 1 root root 4096 Aug 2 07:39 ..
/dev/sdd on /spickzettel type ext4 (ro,relatime,discard,errors=remount-ro,data=ordered)
Der Mount existiert wirklich, read-only, wie angekündigt. Er ist nur leer.
Warum der Mount leer bleibt
Auf dem Node wird die Sache eindeutig. Zuerst die Sicht der Container-Runtime auf das, was sie da geladen hat:
$ crictl images | grep spickzettel
k3d-artefakte:5111/spickzettel v1 44136fa355b36 33.1kB
Die Image-ID lautet 44136fa355b36. Wer den Digest aus dem Manifest oben noch im Kopf hat, erkennt ihn wieder: Das ist sha256:44136fa355b3678a..., der Digest der leeren Config, also von zwei Byte {}. containerd benutzt den Config-Digest als Image-ID, und weil bei einem Artefakt jede Config identisch leer ist, bekommen sämtliche Artefakte auf diesem Node dieselbe ID.
Der zweite Beweis liegt im Snapshotter:
$ ctr -a /run/k3s/containerd/containerd.sock -n k8s.io snapshot ls | grep image-volumes
KEY PARENT KIND
/run/k3s/containerd/io.containerd.grpc.v1.cri/image-volumes/bef9b8b9…/020d7d03… Active
Die mittlere Spalte ist der Parent, und sie ist leer. Ein Snapshot ohne Parent bedeutet: Es wurde kein einziger Layer ausgepackt. Das PDF liegt vollständig im Content Store des Nodes, es ist nur nie zu einer Dateisystemschicht geworden.
Der Grund ist eine Zeile Logik, die man leicht übersieht. containerd packt beim Entpacken eines Images genau die Layer aus, deren Medientyp es kennt, also application/vnd.oci.image.layer.v1.tar, ...tar+gzip und die Docker-Entsprechungen. Ein Layer mit application/pdf ist keiner davon. Er wird nicht abgelehnt, er wird übergangen. Am Ende sind null Layer ausgepackt, das ist kein Fehlerzustand, und der Mount ist leer.
Die Gegenprobe
Damit ist die Diagnose noch nicht scharf. Liegt es am ungewöhnlichen Layer-Medientyp oder an der leeren Config? Also dasselbe PDF ein zweites Mal, diesmal in einem echten tar+gzip-Layer verpackt, aber weiterhin mit leerer Config und eigenem artifactType:
tar czf spickzettel-layer.tar.gz spickzettel.pdf
oras push --plain-http localhost:5111/spickzettel-tar:v1 \
--artifact-type application/vnd.trutz.spickzettel \
spickzettel-layer.tar.gz:application/vnd.oci.image.layer.v1.tar+gzip
Diesmal gibt es keine Stille, sondern einen Anschlag:
Failed to pull image "k3d-artefakte:5111/spickzettel-tar:v1":
failed to unpack image on snapshotter overlayfs:
mismatched image rootfs and manifest layers
Das ist der eigentliche Aha-Moment. Sobald der Layer ein bekannter Typ ist, zählt containerd ihn als rootfs-Layer und vergleicht ihn mit den rootfs.diff_ids in der Config. Nur enthält eine leere Config {} überhaupt kein rootfs, also null diff_ids. Ein Layer gegen null Einträge: Widerspruch, Abbruch.
Beide Fälle haben dieselbe Ursache. Ein OCI-Artefakt ist genau dadurch definiert, dass seine Config leer ist, und eine leere Config kann keine Layer beschreiben. containerd rechnet aber ausschließlich über die Config. Deshalb gibt es für ein Artefakt nur zwei Ausgänge: stumm leer oder harter Fehler. Ein drittes Ergebnis ist strukturell nicht vorgesehen.
Zur Vollständigkeit die dritte Variante, ein normales Image mit demselben Inhalt:
FROM scratch
COPY spickzettel.pdf /spickzettel.pdf
$ kubectl exec spickzettel-image -- md5sum /spickzettel/spickzettel.pdf
2183f0209505c15dd237c6be2932350a /spickzettel/spickzettel.pdf
Byte für Byte das Original. Das Feature selbst funktioniert also tadellos. Es funktioniert nur mit Images.
Das Beispiel aus der Dokumentation ist kein Artefakt
An dieser Stelle wird es unangenehm. Die offizielle Kubernetes-Seite zu Image Volumes zeigt als Beispiel eine Referenz namens quay.io/crio/artifact:v2, und dieses Beispiel läuft unter containerd anstandslos durch. Zwei Dateien erscheinen im Mount. Wer die Doku abarbeitet, sieht ein funktionierendes Artefakt und hakt das Thema ab.
Schaut man sich das Manifest an, ergibt sich ein anderes Bild:
$ oras manifest fetch quay.io/crio/artifact:v2 | jq '{mediaType, artifactType, config: .config.mediaType}'
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"artifactType": null,
"config": "application/vnd.docker.container.image.v1+json"
}
Docker-Manifest-Schema, artifactType nicht gesetzt, eine vollwertige Image-Config, dazu zwei Layer vom Typ application/vnd.docker.image.rootfs.diff.tar.gzip. Das Ding heißt „artifact”, ist aber ein ganz gewöhnliches Container-Image, und zwar nicht einmal ein OCI-, sondern ein Docker-Schema-Image.
Das ist kein Vorwurf an die Autoren. Es zeigt nur, wie unscharf der Begriff inzwischen benutzt wird, und es erklärt, warum die Lücke so selten auffällt: Das kanonische Beispiel umgeht sie.
Wer kann es, wer nicht
Der Zustand ist bekannt und dokumentiert. Im containerd-Projekt läuft dazu seit Anfang 2025 der Eintrag Mount OCI Artifacts, und er ist offen. Ein zweiter Eintrag mit genau dem Fehler aus der Gegenprobe, #11907, wurde als Duplikat davon geschlossen. Es ist also keine Fehlfunktion, sondern eine Funktion, die es noch nicht gibt.
Bei CRI-O sieht es anders aus. Dort gibt es seit Version 1.33 einen eigenen Artefakt-Speicher unterhalb von /var/lib/containers/storage/artifacts und einen ausdrücklichen Schalter oci_artifact_mount_support in der Konfiguration. CRI-O hat das Problem also als eigenes Thema behandelt, statt Artefakte durch die Image-Verarbeitung zu schieben. Ich habe das nicht selbst nachgemessen, mein Versuchsaufbau lief ausschließlich gegen containerd, aber die Trennlinie verläuft erkennbar zwischen den Runtimes und nicht zwischen den Kubernetes-Versionen.
Für die Praxis heißt das: Der Volume-Typ heißt gleich, die Pod-Spezifikation sieht gleich aus, und was dabei herauskommt, hängt an der Runtime unter dem Cluster. Ein Manifest, das auf OpenShift funktioniert, kann auf einem k3s- oder EKS-Cluster still ein leeres Verzeichnis liefern. Genau deshalb ist die Beschriftung „a container image or OCI artifact” in kubectl describe mehr als eine Ungenauigkeit.
Was daraus folgt
Drei Konsequenzen für alle, die das einsetzen wollen.
Liefere Dateien als Image aus, nicht als Artefakt. FROM scratch plus COPY sind zwei Zeilen und funktionieren unter jeder Runtime, die Image Volumes beherrscht. Die Registry ist dieselbe, die Rechteverwaltung ist dieselbe, die Replikation ist dieselbe. Man verliert nur den artifactType, also die Fähigkeit, den Inhalt selbstbeschreibend zu machen. Für einen Volume-Mount ist das verschmerzbar, weil ohnehin der Pod bestimmt, was er erwartet.
Prüfe den Mount, statt dem Status zu vertrauen. Ein Pod im Zustand Running sagt hier nichts aus. Ein Init-Container mit einem test -n "$(ls -A /pfad)" oder ein Readiness-Probe-Skript, das eine erwartete Datei sucht, verwandelt den stummen Fehlschlag in einen lauten. Das ist die eine Zeile, die den Unterschied macht zwischen „fällt beim Deployment auf” und „fällt drei Wochen später auf, weil die Anwendung mit Standardwerten hochgekommen ist”.
Halte Artefakt und Volume auseinander. OCI-Artefakte sind hervorragend geeignet, um Dinge in einer Registry abzulegen und über Werkzeuge wieder herauszuholen, so wie Helm, Trivy oder Cosign es tun. Der Weg ins Dateisystem eines Containers führt bislang über Images.
Fazit
- Der Volume-Typ
imagemountet Container-Images. Echte OCI-Artefakte mit leerer Config mountet er unter containerd nicht, obwohl Dokumentation undkubectl describedas nahelegen. - Der Fehlschlag ist stumm: Pod läuft, Volume ist gemountet, kubelet meldet einen erfolgreichen Pull mit korrekter Größe, das Verzeichnis ist leer. containerd überspringt Layer mit unbekanntem Medientyp, ohne das zu vermerken.
- Mit einem bekannten Layer-Typ und leerer Config gibt es stattdessen
mismatched image rootfs and manifest layers, weil eine leere Config keinerootfs.diff_idsenthält. Beide Ausgänge folgen aus derselben Ursache. - Das Beispiel
quay.io/crio/artifact:v2aus der offiziellen Dokumentation ist kein Artefakt, sondern ein Docker-Schema-Image mit zwei rootfs-Layern. Deshalb funktioniert es und deshalb verdeckt es die Lücke. - Die Trennlinie verläuft zwischen den Runtimes: containerd/containerd#11381 ist offen, CRI-O hat mit
oci_artifact_mount_supporteinen eigenen Weg. Wer plattformübergreifend ausliefert, baut ein Image.
Bleibt eine Beobachtung, die über dieses Feature hinausgeht. Im letzten Beitrag stand, dass die Aussage „OCI-konform” allein wenig verspricht, weil von den vier Kategorien der Distribution-Spezifikation nur eine verbindlich ist. Hier zeigt sich die andere Hälfte desselben Musters: Das Format erlaubt beliebige Artefakte, die Spezifikation beschreibt sie ausdrücklich, und trotzdem entscheidet die Implementierung darunter, ob etwas ankommt. Ein offener Standard sagt, was möglich ist. Er sagt nicht, was deine Runtime tut.
Der Unterschied ist, dass ich es nachsehen konnte. Drei Varianten, ein Nachmittag, ein eindeutiges Ergebnis, und der Grund dafür stand am Ende in einem öffentlichen Issue und in offenem Quelltext. Bei einer geschlossenen Plattform wäre am Ende dieses Nachmittags ein Support-Ticket gestanden. Das ist keine Nebensache, sondern der praktische Wert der zweiten Säule: nicht, dass alles funktioniert, sondern dass du herausfinden kannst, warum nicht.
© 2026 Trutz Software Consulting GmbH. CC BY 4.0: Dieser Text darf mit Namensnennung weiterverwendet und bearbeitet werden, auch kommerziell. Code-Beispiele darin stehen unter Apache 2.0. Ausgenommen sind Marke und Logo sowie gekennzeichnete Zitate Dritter. Was das genau heißt.
Newsletter
Erhalte praxisnahe Tutorials, Best Practices und echte Lösungen für die häufigsten DevOps-Probleme direkt in Dein Postfach.
Datenschutz: Deine E-Mail-Adresse wird DSGVO-konform in Deutschland gespeichert. Keine Spam, nur relevante Inhalte. Eine E-Mail alle 1 bis 2 Wochen.