Cloud-Service- und
Cloud-Technologie-
Provider
Cloud-Service- und
Cloud-Technologie-Provider
Suche

Unsere Mission
Green, Open, Efficient

Blog.

Nutzung von Open Container Initiative (OCI) Container Images als Linux Containern (LXC) in Proxmox VE

Geschrieben von Lucas Trilken

In diesem Blogpost wird gezeigt, wie sich OCI-konforme Container-Images direkt als LXC-Container in Proxmox Virtual Environment (Proxmox VE) verwenden lassen. Es wird dabei insbesondere auf die Portierung der Container und den dauerhaften Betrieb via Systemd eingegangen.

Um die Unterschiede zwischen OCI-Containern und LXC-Containern zu verstehen, lohnt sich ein kleiner Exkurs auf die zugrunde liegenden Virtualisierungskonzepte und ihre jeweiligen Stärken.

Warum nutzen wir Virtualisierung?

Virtualisierung ermöglicht eine bessere Verteilung von Ressourcen und  eine Effizienzsteigerung im Betrieb von Cloud-Infrastrukturen. Darüber hinaus trägt sie zur Stabilisierung und Beschleunigung von Deployment-Prozessen bei, da sich Testumgebungen schneller und einfacher in ihren Ursprungszustand zurückversetzen lassen. Insbesondere profitiert die Softwareentwicklung (ebenso wie die Governance) davon, weil Abhängigkeiten der eingesetzten Software besser bestimmt und passgenau bereitgestellt werden können.

Welche Arten der Virtualisierung gibt es?

Es gibt verschiedene Arten der Virtualisierung, die jeweils auf unterschiedliche Einsatzszenarien zugeschnitten sind. Betriebssystemvirtualisierung wird häufig genutzt, um Legacy-Anwendungen, die sehr spezifische Anforderungen an das Betriebssystem stellen, weiterhin sicher betreiben zu können. Durch die Abstraktion vom zugrunde liegenden physischen System können solche Anwendungen auch auf moderner Hardware lauffähig gemacht werden. Weiterhin trägt diese Virtualisierungsart dazu bei, die Hardwareregeneration sicherzustellen. Technisch umgesetzt wird dies typischerweise mit (Kernel-basierten) virtuellen Maschinen (KVMs) oder falls es sich um Linux basierte Applikationen handelt mit LXC-Containern.

Bei der Anwendungsvirtualisierung liegt der Fokus auf der Isolation einzelner Anwendungen samt ihrer Laufzeitumgebung. Dies hat den Vorteil, dass weniger Systemressourcen benötigt werden, da lediglich die für die Anwendung erforderlichen Softwarekomponenten – in Form des Root-Filesystems (rootfs) – als Paket gebündelt werden. Diese gebündelte Einheit wird dann als geschlossene Anwendung ausgeführt. Typische Vertreter dieser Virtualisierungsform sind OCI-konforme Container-Images, die mit Tools wie Docker, Podman oder systemd-nspawn  gestartet werden können, ebenso aber auch ein klassisches chroot.

Virtualisierungsansatz mit Proxmox VE

Der Ansatz von Proxmox VE setzt primär auf Betriebssystemvirtualisierung und nutzt Anwendungsvirtualisierung üblicherweise erst innerhalb der Gastsysteme. Aus meiner Sicht ist dies grundsätzlich sinnvoll: Es ermöglicht die klare Kapselung von Zuständigkeiten – zwischen dem System, das die eigentlichen Hardwareressourcen verwaltet, und den Gastsystemen, die Laufzeitumgebungen bereitstellen (insbesondere eigenständige Kernel mit dedizierten IP-Stacks). Diese Trennung von Zuständigkeiten und Abhängigkeiten sorgt nicht nur für Stabilität, sondern erleichtert auch die Wartung und Updates des Systems. Damit wird die Grundlage für einen langfristig stabilen und sicheren Betrieb gelegt. Durch die Kapselung wird auch der Hypervisor geschützt, was zusätzlich die Stabilität der parallel ausgeführten Workloads erhöht. Ein weiterer Vorteil ist auch, dass die Transferleistung für den Betrieb der einzelnen Workloads in den VMs niedriger ist, da Betriebssystemvirtualisierung bekannte Konzepte aus dem Betrieb physikalischer Maschinen überträgt.

Vorteile der Virtualisierung und warum sie direkt auf dem Hypervisor eingesetzt wird

Ein anschauliches Beispiel für die Bedeutung der Trennung von Applikationen und der darunterliegenden Hardwareressourcenverwaltung stammt aus meiner Zeit bei einem früheren Arbeitgeber, der über 300 Linux-Server im Einsatz hatte. Dort wurde eine Jira-Applikation betrieben, die von bestimmten Java-Runtime-Versionen abhängig war. Offizieller Support war damals – noch vor meiner Zeit – nur für eine „Enterprise“-Linux-Distribution vorhanden. Als Red Hat-affines Unternehmen entschied man sich für RHEL 5. Die Situation wurde kompliziert, als Jira aus lizenzrechtlichen Gründen nicht mehr updatefähig war. Um die Kompatibilität zur mitgelieferten Java-Runtime zu gewährleisten, wurde die gesamte Betriebssystemversion auf RHEL 5 „eingefroren“ (sehr zur Freude der IT-Security, denn damit lief nach 10 Jahren auch die Versorgung mit Security Patches aus). Weder die Anwendungsbetreiber noch das zuständige Linux-/Unix-Betriebsteam hatten ausreichend Ressourcen, um diese Altlast dauerhaft zu pflegen. Ähnliche Probleme traten auch öfter bei Anwendungen auf, die auf veraltete Python- oder PHP-Versionen angewiesen waren.

Diese Anekdote verdeutlicht, wie wichtig die Anwendungsvirtualisierung ist. In größeren Cloud-Umgebungen bieten sich hierfür Lösungen wie Tarook , ein Open-Source Lifecycle Management Tool für Kubernetes, an. In kleineren Setups hingegen sind Werkzeuge wie Docker oder Podman, betrieben innerhalb von Virtuellen Machines, alternative Lösungen. Auch dieser Ansatz profitiert dabei von den Hochverfügbarkeitsmechanismen der Proxmox VE Lösung.

Ein zentraler Vorteil der Anwendungsvirtualisierung, insbesondere durch den Einsatz von OCI-Images, liegt in der Standardisierung. Diese Standardisierung und der Erfolg von Docker sowie vergleichbaren Lösungen haben ein breit gefächertes Ökosystem hervorgebracht, welches es ermöglicht, getestete Anwendungen schnell und einfach über Container-Registries zu verteilen.

Als Unternehmen, das Kubernetes mit OCI-basierte Images für die Bereitstellung der eigenen OpenStack-Deployments bereits verwendet, bietet es sich für Cloud&Heat an, Lösungen, die auf diese Weise bereits bereitgestellt werden, auch für die Konfiguration von Proxmox-VE-Deployments zu verwenden. Obwohl die Proxmox VE Umgebungen selbst keine direkte Abhängigkeit zu OCI-Images haben, werden diese intern verwendet, um vorhandene Lösungen (z.B. für das Usermanagement) zu portieren. Praktischerweise bieten die von Proxmox VE mitgelieferten LXC-Services hierfür ein Kompatibilitätslayer an.

Implementierung von OCI-Images als LXC-Containern in Proxmox VE

Technische Voraussetzungen

Um OCI-Images aus Registries beziehen zu können, ist unter Proxmox VE die Installation der Pakete skopeo und umoci notwendig:

apt install skopeo umoci

Nach der Installation finden sich auf dem System einige typische Konfigurationsdateien, wie man sie in ähnlicher Form bereits von Docker- oder Podman-Installationen kennt. Ein Beispiel dafür ist die Datei /var/lib/containers/cache/blob-info-cache-v1.boltdb. Diese Datei wird verwendet, um die OCI-Layer zu tracken.

Weiterhin finden sich Konfigurationen zu sogenannten „Well-Known“ Images und Registries unter:

root@pve:~# tree /etc/containers/
/etc/containers/
├── policy.json
├── registries.conf
└── registries.conf.d
└── shortnames.conf

Und schließlich besteht auch die Möglichkeit, Credentials für protected Registries zu hinterlegen unter:

root@pve:~# cat ~/.docker/config.json
{"auths":
{  "<registry url e.g. gittlab-registry.cloudandheat.com>":
{ "auth": "<token_name + token_value | b64encoded>" } }
}

 

Konfiguration der Container

Nach der Konfiguration der Containerquellen ist es nun möglich, über die LXC cli tools entsprechende Container zu erstellen. Die LXC-Container, die via der Proxmox VE WebUI bzw. dem pct cli tool erstellt werden, werden ebenfalls als LXC-Container unter /var/lib/lxc/ erstellt. Sie werden durch die Proxmox VE- eigenen Verwaltungstools so konfiguriert, dass die LXC-Services sie starten und verwalten können.

Dieses Vorgehen ist auch für die Konfiguration der LXC-Container aus OCI-Images notwendig, wenn man nicht den Komfort der PVE-Tools nutzt. Container, die über die PVE-Tools erstellt werden, werden im Übrigen unter /var/lib/lxc/<id> erstellt. Dies sollte man bei der Erstellung weiterer manueller LXC-Container beachten.

Ein weiterer wichtiger Punkt: Die PVE Tools ignorieren die Standardwerte, die für neu erstellte LXC-Container aus der Datei /etc/lxc/default.conf übernommen werden. Das bedeutet, dass globale Anpassungen für Default-Werte hier durchaus gesetzt werden könnten, ohne mit den Proxmox-Containern zu kollidieren.

Wenn wir nun einen neuen LXC-Container mit der Option –template=oci erstellen, z.B.:

root@pve:~# lxc-create --name bash --template=oci -- --url docker://bash:devel-alpine3.22
Copying blob ed9f46a56c4c skipped: already exists
Copying blob 9824c27679d3 skipped: already exists
Copying blob 1926526997ec skipped: already exists
Copying blob 429cd906597a skipped: already exists
Copying config 376e768ec4 done
Writing manifest to image destination
Storing signatures
mfpath=/var/lib/lxc/bash/oci/blobs/sha256/8c4dd3f704a443e34a1aa9952b304de5148c54388ff6aed80d0a2b06db1da65d conf=/var/lib/lxc/bash/oci/blobs/sha256/376e768ec4e15b92ee8ddb2ea388c2418902684b24eb8ef58421ba85478ab296
mediatype=application/vnd.oci.image.layer.v1.tar+gzip
Unpacking tar rootfs

wird die entsprechende Container-Konfiguration unter /var/lib/lxc/<container_name> erzeugt:

root@pve:~# ls /var/lib/lxc/bash
config  oci  rootfs

Beim Erstellen sollte man sich von der URL-Option nicht verunsichern lassen: „docker://“ spezifiziert hierbei das verwendete Protokoll, um die Registry zu erreichen. Danach kann jede URL verwendet werden, die sich gemäß der Konfiguration unter /etc/containers/registries.conf* auflösen lässt. Hierfür wird skopeo verwendet, das auch weiterführende Informationen auf den Manpages bereitstellt.

Nach erfolgreicher Erstellung eines Containers findet sich im Pfad /var/lib/lxc/<container_name> folgende Struktur:

  • den Ordner oci, der die Blobs sowie die dazugehörigen Informationen enthält
  • den Ordner rootfs, der die aus den Blobs zusammengesetzte Ordnerstruktur für unseren LXC-Container enthält
  • die config-Datei, die Informationen für LXC bereithält, wie der entsprechende Container zur Laufzeit zu konfigurieren ist. Direkt nach der Erstellung via lxc-create findet sich dort eine Kombination aus den Werten in /etc/lxc/default.conf und den Informationen, die sich aus dem Image ableiten lassen. In den meisten Fällen ist dies allerdings nicht direkt lauffähig:
root@pve:~# cat /var/lib/lxc/bash/config
# Template used to create this container: /usr/share/lxc/templates/lxc-oci
# Parameters passed to the template: --url docker://bash:devel-alpine3.22
# For additional config options, please look at lxc.container.conf(5)

# Uncomment the following line to support nesting containers:
#lxc.include = /usr/share/lxc/config/nesting.conf
# (Be aware this has security implications)

## generated by Ansible ch-docker-2-lcx - do not change manually
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
lxc.net.0.hwaddr = 00:16:3e:f3:c8:35
lxc.rootfs.path = dir:/var/lib/lxc/bash/rootfs
lxc.execute.cmd = '"docker-entrypoint.sh"  "bash" '
lxc.mount.auto = proc:mixed sys:mixed cgroup:mixed
lxc.environment = PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
lxc.environment = _BASH_COMMIT=cf8a2518c8b94f75b330d398f5daa0ee21417e1b
lxc.environment = _BASH_VERSION=devel-20250918
lxc.include = /usr/share/lxc/config/common.conf
lxc.include = /usr/share/lxc/config/oci.common.conf
lxc.uts.name = bash
lxc.init.uid = 0
lxc.init.gid = 0
lxc.init.cwd = /

Zum Vergleich anbei noch die verwendete Datei unter /etc/lxc/default.conf auf dem Testsystem:

root@pve:~# cat /etc/lxc/default.conf
## generated by Ansible ch-docker-2-lcx - do not change manually
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
lxc.net.0.hwaddr = 00:16:3e:xx:xx:xx

Um einen lauffähigen Container zu erhalten, muss die config-Datei angepasst werden. Jede inkompatible Konfiguration kann zu einem nicht lauffähigen Container führen. Dies umfasst aufgrund der Default-Konfigurationswerte meistens mindestens das Netzwerkinterface lxc.net.0. Für eine vollständige und korrekte Konfiguration lohnt sich ein Blick in die offizielle Dokumentation:

Dort findet man eine umfassende Übersicht aller verfügbaren Konfigurationsoptionen. Außerdem erhält man gültige Keys zur Konfiguration von Umgebungsvariablen, die im OCI-Kontext häufig für lauffähige Container benötigt werden, oder für durchgereichte Pfade als persistente Volumes.

Bei der Suche nach möglichen Fehlerursachen, bietet es sich an, die Container im Vordergrund mit den entsprechenden Logoptionen zu starten, um diese analysieren zu können. Zum Beispiel:

root@pve:~# lxc-start bash --foreground --logpriority=DEBUG --logfile=~/test.log
can't run '/sbin/openrc': No such file or directory
can't run '/sbin/openrc': No such file or directory
can't run '/sbin/openrc': No such file or directory
can't open /dev/tty5: No such file or directory
can't open /dev/tty6: No such file or directory
can't open /dev/tty5: No such file or directory

Das weist darauf hin, das in unserem Beispiel die Datei /sbin/openrc innerhalb des Containers nicht gefunden wurde. Eine kurze Überprüfung des Containers im rootfs ergibt, dass diese executable auch nicht vorhanden ist:

root@pve:~# ls /var/lib/lxc/bash/rootfs/sbin/openrc
ls: cannot access '/var/lib/lxc/bash/rootfs/sbin/openrc': No such file or directory

Für die Kompatibilität mit zukünftigen Versionen des OCI-Images wäre es hilfreich, das ursprüngliche initiale Startkommando durch ein geeignetes Kommando zu ersetzen:

## Konfiguration nach der Erstellung
lxc.execute.cmd = '"docker-entrypoint.sh"  "bash" '

## Startfähige Konfiguration
lxc.init.cmd = /bin/sh

Anbei befindet sich eine lauffähige Minimalkonfiguration nach den Anpassungen für unser Beispiel:

root@pve:~# cat /var/lib/lxc/bash/config
## generated by Ansible ch-docker-2-lcx - do not change manually
# Template used to create this container: /usr/share/lxc/templates/lxc-oci
# Parameters passed to the template: --url docker://bash:devel-alpine3.22
# For additional config options, please look at lxc.container.conf(5)
# Uncomment the following line to support nesting containers:

lxc.net.0.type = none
lxc.rootfs.path = dir:/var/lib/lxc/bash-example/rootfs
lxc.environment = USER=root
lxc.include = /usr/share/lxc/config/common.conf
lxc.include = /usr/share/lxc/config/oci.common.conf
lxc.init.cmd = /bin/sh
lxc.uts.name = bash-example
lxc.environment = USER=root
lxc.init.uid = 0
lxc.init.gid = 0
lxc.init.cwd = /

Mit der angepassten Konfiguration ist der Container lauffähig und kann über die LXC-Tools verwaltet werden:

# Starten des Containers
root@px121:~# lxc-start --name=bash

# Abruf der Laufzeit informationen
root@px121:~# lxc-info --name=bash
Name:           bash
State:          RUNNING
PID:            33520
IP:             10.10.10.121
IP:             10.10.11.121
IP:             10.10.12.121

# Ausführen von Commands innerhalb des Containers
root@px121:~# lxc-attach --name=bash
~ # echo "Hello World"
Hello World
~ # exit

# Der Container stoppt nicht automatisch nach dem Verlassen
root@px121:~# lxc-info --name=bash
Name:           bash
State:          RUNNING
PID:            33520
IP:             10.10.10.121
IP:             10.10.11.121
IP:             10.10.12.121

# Stop des Containerprozesses (--kill für ein hartes Beenden)
root@px121:~# lxc-stop --name=bash

root@px121:~# lxc-info --name=bash
Name:           bash
State:          STOPPED

# Direktes Starten und Ausführen eines spezifischen Kommandos.

root@px121:~# lxc-execute --name=bash -- echo "hello world"
hello world
root@px121:~# lxc-info --name=bash
Name:           bash
State:          STOPPED

Durch das direkte Starten und Ausführen eines einzelnen Kommandos können OCI-Images portiert werden, um fertig verpackte Programme mithilfe von Aliasen wie native Kommandos direkt auf dem Host auszuführen:

root@px121:~# alias hello-from-oci="lxc-execute --name=bash -- echo 'hello from oci'"
root@px121:~# hello-from-oci
hello from oci

Eine weitere Option ist es, die adaptierten LXC-Container als Daemons laufen zu lassen. Dafür gibt es die Möglichkeiten, entweder lxc-autostart oder Systemd zu verwenden:

  • lxc-autostart: Durch einen einfachen Eintrag der Zeile lxc.start.auto = 1 in der config-Datei des jeweiligen Containers wird sichergestellt, dass der Container nach dem Boot ebenfalls gestartet wird. Erweitere Optionen, etwa für verzögerte Starts und Abhängigkeiten, lassen sich unter https://linuxcontainers.org/lxc/manpages//man5/lxc.container.conf.5.html#lbBK einsehen. Wichtig zu beachten ist, dass lxc-autostart Container nicht neu startet, wenn diese beispielsweise durch einen Fehler oder manuelle Eingriffe gestoppt wurden. Dieses Verhalten kann in einige Fälle, z.B. für Systemdienste, explizit gewünscht sein. Daher zeigen wir im nächsten Abschnitt, wie die LXC-Container mit Systemd verwaltet werden können.
  • Systemd: Systemd bietet die Möglichkeit, mit Template-Files zu arbeiten. Diese sind durch die Verwendung des ‚@‘-Zeichens vor dem eigentlichen Systemd-Unit-File-Endung markiert. Dies können genutzt werden, indem wir ein einzelnes Template für unsere Services in /etc/systemd/system erzeugen:
root@px121:~# cat /etc/systemd/system/lxc-oci@.service
[Unit]
Description=%i run from OCI as LXContainer via systemd
Documentation="https://gitlab.com/cloudandheat"
ConditionPathExists=/var/lib/lxc/%i
Wants=lxc.service
After=lxc.service

[Service]
Environment="START_ARGS=--name=%i --pidfile=/var/lib/lxc/%i/pid"
Environment="STOP_ARGS=--name=%i"
ExecStart=/usr/bin/lxc-start $START_ARGS
ExecStop=/usr/bin/lxc-stop $STOP_ARGS
Restart=always
PIDFile=/var/lib/lxc/%i/pid
Type=simple

[Install]
WantedBy=multi-user.target

Es gibt in den Systemd-Files einige spezifische Variablen, z.B. wird %i durch die escapte Variante der Service-Unit hinter dem ‚@‘ ersetzt. Damit können Systemd-Services auf einfache Weise generiert werden:

root@px121:~# systemctl start lxc-oci@bash.service
root@px121:~# systemctl status lxc-oci@bash.service
● lxc-oci@bash.service - bash run from OCI as LXContainer via systemd
       Loaded: loaded (/etc/systemd/system/lxc-oci@.service; disabled; preset: enabled)
       Active: active (running) since Wed 2025-10-01 13:24:55 CEST; 5s ago
       Docs: https://gitlab.com/cloudandheat
       Main PID: 3960 (lxc-start)
       Tasks: 0 (limit: 3472)
       Memory: 484.0K
       CPU: 317ms
       CGroup: /system.slice/system-lxc\x2doci.slice/lxc-oci@bash.service
               ‣ 3960 "[lxc monitor] /var/lib/lxc bash"

Durch die Verwendung der PID-File-Direktive im Systemd-Service-Template und im ExecStart- Kommando, hat der Systemd-Service nun die Möglichkeit zu erkennen, ob der Prozess gestoppt wurde, ohne das Systemd als zentraler Verwaltungsservice darüber informiert wurde.

 
Deployments via Ansible

Da wir unser Workloads selbstverständlich nicht händisch konfigurieren, haben wir für den oben beschriebenen Ablauf selbstverständlich in einer Ansible Rolle erstellt. Als Unternehmen, das von den Vorteilen von Open Source überzeugt ist, stellen wir das unter folgendem Link als Git-Repository bereit (https://gitlab.com/cloudandheat/docker-2-lxc).

Fazit

Die Nutzung von OCI-Images als LXC-Container eröffnet sich eine weitere Möglichkeit, Anwendungsvirtualisierung mit Proxmox VE zu kombinieren. Durch die Wiederverwendung standardisierter Container-Workflows lassen sich bestehende Images dabei effizient integrieren. Zwar erfordert die manuelle Konfiguration etwas technisches Verständnis, doch bietet sie zugleich Kontrolle und Anpassbarkeit als Vorteile. 

Weitere Blogbeiträge

Cloud and Heat | IaaS Kosten | OpenStack
Cloud-Kosten verstehen: Warum gestoppte Instanzen in OpenStack weiter Kosten verursachen – und wie Shelving Ressourcen spart.
Step-by-Step Guideline für die Erstellung eines Proxmox-Test-Clusters mit High Availability per WebGUI
Proxmox VE als schlanke Alternative für kleine On-Premises-Umgebungen: In unserem Blog zeigen wir dir, wie du ein mobiles Testcluster mit drei Knoten aufsetzt – inklusive erster Schritte zu Hochverfügbarkeit mit Corosync und Ceph.