Podman

Podman ist ein OCI-kompatibles Container-Management-Tool, dessen CLI explizit mit Docker kompatibel ist, was den Übergang für Docker-Nutzer erleichtert. Das Projekt wurde 2017 ins Leben gerufen und ursprünglich von Red Hat entwickelt. Es bietet erweiterte Sicherheitsfunktionen und unterstützt rootless Container. Im Gegensatz zu Docker benötigt Podman keinen Daemon, wodurch keine Privilegieneskalation für Benutzer in der Docker-Gruppe möglich ist.

Beim Umgang mit Podman stösst man auch auf folgende Tools:

  • buildah: zum Bauen von Container Images

  • podman: verwaltet Container und Container Images

  • skopeo: zur Signierung, zum Kopieren und Löschen von Container Images

podman Cheat Sheet

dnf install -y podman systemd-container

podman login $registry
podman search --no-trunc mariadb-103

# inspect without pulling first
skopeo inspect docker://$registry/rhel8/mariadb-103
# local
podman inspect $registry/rhel8/mariadb-103

podman pull $registry/rhel8/mariadb-103
podman images

podman run \
    --detach \
    --name=myname \
    --publish localport:containerport \
    --volume localdir:containerdir:z \
    --env ENV1=VAL1 \
    docker://$registry/rhel8/mariadb-103:tag

podman logs --follow myname

# open shell in running container
podman exec -it myname /bin/bash

# execute and delete
podman run --rm -it $registry/rhel8/mariadb-103 /bin/bash

podman ps --all
podman stop myname
podman kill myname
podman restart myname
podman info
podman rmi $registry/rhel8/mariadb-103:latest
podman rm myname

Im Unterschied zu Docker ein spezielles Feature von Podman: Rootless-Container „mariadb“ als User. Wichtig: statt sudo oder su sollte machinectl verwendet werden.

machinectl shell user@.host

podman run --detach $registry/rhel8/mariadb-103
podman ps

Container als Systemd-Service

Da Container unter Podman nicht über einen Daemon, sondern als einzelne Prozesse laufen, ist die Integration in systemd einfach. Pro Container wird ein Systemd-Service erstellt. Das funktioniert für rootful und rootless Container.

Zuerst muss der Container manuell erstellt werden:

podman create \
    --name httpd \
    --publish 127.0.0.1:8080:80/tcp \
    httpd:latest

podman ps --all

Der Systemd-Service kann nun generiert werden. Der Parameter --new sorgt dafür, dass der Container bei jedem Neustart des Services neu erstellt wird. Dies ist später für automatische Updates des Containers nützlich.

Unter dem root-User:

podman generate systemd httpd --new > /etc/systemd/system/httpd-container.service
systemctl daemon-reload

# double check result
systemctl cat httpd-container.service

systemctl enable --now httpd-container.service
systemctl status httpd-container.service
podman ps

Rootless:

loginctl enable-linger user

machinectl shell user@.host

mkdir -p ~/.config/systemd/user/
podman generate systemd httpd --new > ~/.config/systemd/user/httpd-container.service
systemctl --user daemon-reload

# double check result
systemctl --user cat httpd-container.service

systemctl --user enable --now httpd-container.service
systemctl --user status httpd-container.service
podman ps

Tipp

Der Exit Code der Applikation im Container wird richtig an Systemd weitergegeben und ist via systemctl status ... ersichtlich.

Automatische Updates der Container

Bei der Entwicklung von Podman wurde auch an die automatische Aktualisierung der Container gedacht. Podman wird mit der Funktion auto-update ausgeliefert, die alle markierten Container auf neue Versionen überprüft.

Damit dies funktioniert muss der Container in einem mit podman generate systemd --new generiertem Systemd-Service laufen und das --label "io.containers.autoupdate=registry" gesetzt haben.

podman create \
    --name httpd \
    --publish 127.0.0.1:8080:80/tcp \
    --label "io.containers.autoupdate=registry" \
    docker.io/httpd:latest

podman generate systemd httpd --new > /etc/systemd/system/httpd-container.service

Das Update kann manuell ausgeführt werden:

podman auto-update

Oder als Systemd-Timer (standardmässig täglich):

# rootful
systemctl enable --now podman-auto-update.timer

# rootless
systemctl --user enable --now podman-auto-update.timer

Healthchecks

Nur weil der Container (bzw. der Systemd-Service) gestartet wurde, heisst noch lange nicht, dass auch die Applikation innerhalb des Container bereit ist. Um dies zu prüfen können Healthchecks verwendet werden.

Ein Healthchecks besteht aus:

  • „Command“: Wird innerhalb des Containers ausgeführt. Muss den Status der Applikation ermitteln und als Exit-Code zurückliefern geben (0 = ok, sonst „failure“).

  • „Retries“: Anzahl der aufeinanderfolgenden Fehlversuche bevor der Container als „unhealthy“ markiert wird.

  • „Interval“: Zeitabstand zwischen den Ausführungen des Commands.

  • „Start-period“: Zeitraum, in dem die Healthchecks nach dem Start des Containers fehlschlagen dürfen („grace period“).

  • „Timeout“: Timeout des Commands selbst.

  • „Action“: Wie soll ein „unhealthy“ Container behandelt? Optionen: none, kill, restart und stop. Zusammen mit Systemd ist kill am sinnvollsten - ein Restart kann dann via Systemd Restart=on-failure erfolgen.

podman run \
    --detach \
    --name mariadb \
    --publish 127.0.0.1:3306:3306 \
    --env MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=1 \
    --health-cmd 'mysql --execute ";"' \
    --health-retries 5 \
    --health-interval 10s \
    --health-start-period 5s \
    --health-timeout 3s \
    --health-on-failure=kill \
    docker.io/library/mariadb:10.5

Die Healthchecks können auch manuell ausgeführt werden:

podman healthcheck run mariadb
echo $?

Eigene Container bauen

Hier am Beispiel von einem Icinga2 Agent Container, mit Systemd. Es wird angenommen, dass der Container auf dem Icinga2 Master laufengelassen wird.

Containerfile
FROM docker.io/library/rockylinux:9 AS builder

RUN mkdir -p /mnt/rootfs

# install prerequisites including systemd
RUN dnf --installroot /mnt/rootfs install --releasever 9 --setopt install_weak_deps=false --nodocs --assumeyes systemd curl-minimal && \
    dnf --installroot /mnt/rootfs clean all

# Icinga2 Installation
RUN curl https://packages.icinga.com/icinga.key --output /etc/pki/rpm-gpg/RPM-GPG-KEY-ICINGA
COPY ICINGA-release.repo /etc/yum.repos.d/ICINGA-release.repo
RUN dnf repolist && dnf --installroot /mnt/rootfs install --releasever 9 --setopt install_weak_deps=false --nodocs --assumeyes icinga2 && \
    dnf --installroot /mnt/rootfs clean all

# Linuxfabrik Monitoring Plugins
RUN rpm --import https://repo.linuxfabrik.ch/linuxfabrik.key
RUN curl https://repo.linuxfabrik.ch/monitoring-plugins/rhel/linuxfabrik-monitoring-plugins-release.repo --output /etc/yum.repos.d/linuxfabrik-monitoring-plugins-release.repo
RUN dnf --installroot /mnt/rootfs install --releasever 9 --setopt install_weak_deps=false --nodocs --assumeyes linuxfabrik-monitoring-plugins sudo procps && \
    dnf --installroot /mnt/rootfs clean all

# clean up
RUN rm -rf /mnt/rootfs/var/cache/* /mnt/rootfs/var/log/dnf* /mnt/rootfs/var/log/yum.* /mnt/rootfs/var/lib/dnf/*


# ---------------------------------
FROM docker.io/library/rockylinux:9

COPY --from=builder /mnt/rootfs/ /

# Ensure proper shutdown handling for systemd
STOPSIGNAL SIGRTMIN+3

# Enable the Icinga2 service in systemd by creating the required symlink
RUN ln -s /usr/lib/systemd/system/icinga2.service /etc/systemd/system/multi-user.target.wants/icinga2.service

# Expose the Icinga2 agent port
EXPOSE 5665

COPY entrypoint /usr/local/bin/entrypoint
RUN chmod +x /usr/local/bin/entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint"]

Ausserdem braucht es folgende Dateien:

entrypoint
#!/usr/bin/env bash

set -e -x

icinga2 pki save-cert \
    --host host.containers.internal \
    --port 5665 \
    --key local.key \
    --cert local.crt \
    --trustedcert /var/lib/icinga2/certs/master.crt

# Build the base node setup command
icinga2_params="--zone icinga-demo-rocky9 \
    --endpoint icinga-demo.linuxfabrik.ch,host.containers.internal,5665 \
    --parent_host host.containers.internal,5665 \
    --parent_zone master \
    --cn icinga-demo-rocky9 \
    --accept-config \
    --accept-commands \
    --disable-confd \
    --trustedcert /var/lib/icinga2/certs/master.crt"

# If ICINGA2_TICKET is set, add the --ticket option
if [ -n "$ICINGA2_TICKET" ]; then
    icinga2_params+=" --ticket $ICINGA2_TICKET"
fi

# Execute the node setup command
echo "Running command: icinga2 node setup $icinga2_params"
icinga2 node setup $icinga2_params

# Finally, start systemd as PID 1
exec /usr/sbin/init
ICINGA-release.repo
[icinga-stable-release]
name=ICINGA (stable release)
baseurl=https://packages.icinga.com/subscription/rhel/$releasever/release/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-ICINGA

# NOTE: This repo requires an Icinga Subscription. Have a look at https://www.linuxfabrik.ch/en/products/icinga-subscriptions
# fill in username and password
# username=
# password=

Nun kann der Container gebaut und gestartet werden:

podman build --file Containerfile --tag icinga2-agent-rocky9

podman run \
    --detach \
    --name icinga2-agent-rocky9 \
    --hostname icinga2-agent-rocky9 \
    --publish 5666:5665 \
    --systemd=always \
    icinga2-agent-rocky9:latest

Nun muss auf dem Master per icinga2 ca list und icinga2 ca sign das Zertifikat signiert werden. Will man dies vermeiden, so kann ein Ticket verwendet werden:

icinga2_api_user='ticket-user'
icinga2_api_pw='linuxfabrik'
icinga2_agent_cn='icinga2-agent-rocky9'
cat > /tmp/icinga2-api.json << EOF
{
    "cn": "$icinga2_agent_cn"
}
EOF

ICINGA2_TICKET=$(curl \
--insecure \
--user "$icinga2_api_user:$icinga2_api_pw" \
--header 'Accept: application/json' \
--request POST \
'https://localhost:5665/v1/actions/generate-ticket' \
--data @/tmp/icinga2-api.json | jq --raw-output '.results[0].ticket')

podman run \
    --detach \
    --name "$icinga2_agent_cn" \
    --hostname "$icinga2_agent_cn" \
    --publish 5666:5665 \
    --systemd=always \
    --env ICINGA2_TICKET="$ICINGA2_TICKET" \
    icinga2-agent-rocky9:latest

Troubleshooting

ERRO[0000] unable to get systemd connection to add healthchecks: lstat /tmp/podman-run-1001/systemd: no such file or directory
dnf install -y systemd-container
machinectl shell user@.host

Alternativ manuell:

sudo -u user -i
export XDG_RUNTIME_DIR=/run/user/$(id -u)

Built on 2025-03-27