Zum Hauptinhalt springen

Podman auf Raspberry Pi mit Ansible

·1288 Wörter·7 min
Inhaltsverzeichnis
Raspberry Pi Homelab - Dieser Artikel ist Teil einer Serie.
Teil 1: Dieser Artikel

Der Raspberry Pi Zero 2 W ist mit seinen 512 MB RAM und der Quad-Core CPU ein sparsamer aber leistungsfähiger Kandidat für kleine Container-Workloads im Homelab. Dieser Artikel zeigt, wie du rootless Podman mit funktionierenden CPU- und Memory-Limits vollautomatisiert mit Ansible einrichtest.

Zum Einsatz kommt die Ansible-Rolle head1328.podman , die Podman rootless für einen dedizierten User (z.B. podman) einrichtet. Mit der Option podman_restrict_user=true wird sichergestellt, dass nur dieser User Podman ausführen darf. Debian 13 (Trixie) bringt Podman 5 mit, das native Quadlet-Unterstützung für systemd-Integration bietet. Darauf gehen wir im nächsten Artikel der Serie ein.

Voraussetzungen
#

Voraussetzung: Auf dem Raspberry Pi Zero 2 W ist bereits Raspberry Pi OS (Debian 13 / Trixie) mit dem Raspberry Pi Imager installiert. Dabei wurde ein SSH-Key hinterlegt, der Hostname auf pi-hole-primary gesetzt und ein User ansible mit sudo-Rechten angelegt. Der Pi ist per USB-Ethernet-Adapter am LAN angeschlossen und über pi-hole-primary.fritz.box erreichbar. Tutorials zur Grundeinrichtung des Raspberry Pi gibt es reichlich im Netz, das überspringen wir hier.

Du brauchst Ansible lokal, alternativ den Ansible Dev Container ghcr.io/ansible/community-ansible-dev-tools:latest.

Dieses Tutorial ist für macOS und eine FritzBox als Router geschrieben. Mit anderen Betriebssystemen oder Routern sollte es aber keine großen Unterschiede geben.

Ansible-Projekt aufsetzen
#

Das komplette Projekt findest du hier:

Dieses Tutorial basiert auf der Version 1.0.0 .

Projektstruktur
#

ansible/
├── ansible.cfg
├── requirements.yml
├── play.yml
└── inventories/
    └── homelab/
        ├── hosts.yml
        └── group_vars/
            ├── all.yml
            └── podmen.yml

ansible.cfg
#

[defaults]
inventory = ./inventories/homelab/hosts.yml
ask_vault_pass = true
remote_user = ansible
private_key_file = ~/.ssh/id_ed25519

# allow becoming non privileged user
allow_world_readable_tmpfiles=true

requirements.yml
#

---
collections:
  - name: devsec.hardening
    version: ">=10.4.0,<10.5"
    type: galaxy
  - name: containers.podman
    version: ">=1.18.0,<1.19"
    type: galaxy

roles:
  - name: head1328.podman
    version: "v0.1.0"
  - name: escalate.swap
    version: "v2.1.0"

Abhängigkeiten installieren (Rollen und Collections):

ansible-galaxy install -r requirements.yml

Inventory: hosts.yml
#

---
all:
  vars:
    ansible_python_interpreter: /usr/bin/python3
  children:
    podmen:
      hosts:
        pi-hole-primary:
          ansible_host: pi-hole-primary.fritz.box

Group Vars: podmen.yml
#

---
podman_user_name: podman
podman_user_group: podman

Group Vars: all.yml
#

---
ansible_become_pass: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  ...

Das sudo-Passwort verschlüsseln:

# interactive mode (empfohlen)
ansible-vault encrypt_string

# Alternative
ansible-vault encrypt_string 'dein-sudo-passwort' --name ansible_become_pass

Playbook erstellen
#

play.yml
#

---
- name: Podman Setup
  hosts: podmen
  handlers:
    - name: Reload systemd
      ansible.builtin.systemd:
        daemon_reload: true
      become: true

    - name: Reboot Raspberry Pi
      become: true
      ansible.builtin.reboot:
        msg: "Reboot required"
        connect_timeout: 5
        reboot_timeout: 600
        pre_reboot_delay: 2
        post_reboot_delay: 10
        test_command: whoami

  pre_tasks:
    - name: Setup swap (optional for 512 MB RAM)
      become: true
      tags:
        - swap
      block:
        - name: Include escalate.swap role
          ansible.builtin.include_role:
            name: escalate.swap
          vars:
            swap_enabled: true
            swap_config:
              CONF_SWAPSIZE: 256

    - name: Protect critical system services from CPU starvation
      become: true
      block:
        - name: Ensure systemd drop-in directories for critical services
          ansible.builtin.file:
            path: "/etc/systemd/system/{{ item }}.service.d"
            state: directory
            owner: root
            group: root
            mode: '0755'
          loop:
            - ssh
            - systemd-journald
            - dbus

        - name: Set high CPUWeight for critical services
          ansible.builtin.copy:
            dest: "/etc/systemd/system/{{ item }}.service.d/cpu-weight.conf"
            owner: root
            group: root
            mode: '0644'
            content: |
              [Service]
              CPUWeight=1000
          loop:
            - ssh
            - systemd-journald
            - dbus
          notify: Reload systemd

    - name: Ensure kernel cmdline has required cgroup flags
      become: true
      tags:
        - podman
      block:
        - name: Read kernel cmdline
          ansible.builtin.slurp:
            path: /boot/firmware/cmdline.txt
          register: cmdline_raw

        - name: Build normalized kernel cmdline
          ansible.builtin.set_fact:
            cmdline_new: >-
              {{
                (cmdline_raw.content | b64decode | trim)
                | regex_replace('\s*cgroup_enable=memory\s*', ' ')
                | regex_replace('\s*cgroup_memory=1\s*', ' ')
                | regex_replace('\s*swapaccount=1\s*', ' ')
                | regex_replace('\s+', ' ')
                | trim
              }}
              cgroup_enable=memory cgroup_memory=1 swapaccount=1

        - name: Write kernel cmdline (exactly one line)
          ansible.builtin.copy:
            dest: /boot/firmware/cmdline.txt
            content: "{{ cmdline_new }}"
            owner: root
            group: root
            mode: '0644'
          notify: Reboot Raspberry Pi

  roles:
    - role: head1328.podman
      become: true
      tags:
        - podman

Playbook ausführen
#

ansible-playbook play.yml

Das Playbook:

  1. Richtet Swap ein (optional, aber empfohlen bei 512 MB RAM)
  2. Schützt kritische Services (SSH, journald, dbus) vor CPU-Starvation
  3. Konfiguriert die Kernel-Parameter für cgroup v2 Memory-Controller
  4. Installiert Podman und richtet einen dedizierten podman-User ein
  5. Aktiviert die systemd-Delegation für rootless Container
  6. Startet den Raspberry Pi neu (falls Kernel-Parameter geändert wurden)

Was die head1328.podman Rolle macht
#

Die Rolle automatisiert folgende Schritte:

SchrittBeschreibung
Podman installierenapt install podman
User erstellenSystem-User podman mit Gruppe podman
ZugriffsrechtePodman-Binary nur für podman-Gruppe ausführbar
subuid/subgidUser-Namespaces für rootless Container konfigurieren
Lingeringloginctl enable-linger für User-Services ohne Login
Delegationsystemd-Drop-in für cgroup-Controller: cpu cpuset memory io pids
SocketPodman-User-Socket aktivieren und starten

Die delegate.conf
#

Die Rolle erstellt /etc/systemd/system/user@.service.d/delegate.conf:

[Service]
Delegate=yes
Controllers=cpu cpuset memory io pids

Diese Konfiguration ist entscheidend für funktionierende Ressourcen-Limits in rootless Containern.

Damit ist die Einrichtung abgeschlossen und Podman läuft. Im Folgenden verifizieren wir die Installation.

Installation verifizieren
#

Nach dem Playbook-Lauf verbindest du dich per SSH auf den Pi und prüfst:

cgroup v2 aktiv?
#

stat -fc %T /sys/fs/cgroup
# Erwartet: cgroup2fs

sudo -u podman bash -c 'cd "$HOME" && podman info --format "{{ .Host.CgroupsVersion }}"'
# Erwartet: v2

Controller für User verfügbar?
#

cat /sys/fs/cgroup/user.slice/user-$(id -u podman).slice/cgroup.controllers
# Muss enthalten: cpuset cpu io memory pids

Delegation aktiv?
#

systemctl show user@$(id -u podman) -p Delegate
# Erwartet: Delegate=yes

CPU-Limits testen
#

Mit stress-ng kannst du CPU-Limits verifizieren:

sudo -u podman bash -c 'cd "$HOME" && podman run --rm \
  --name=stressng \
  --cpus=0.5 \
  alpine \
  sh -c "apk add --no-cache stress-ng && stress-ng --cpu 1 --timeout 30"'

In einem zweiten Terminal beobachtest du:

sudo -u podman bash -c 'cd "$HOME" && podman stats'

Erwartung: max. ~50% eines CPU-Kerns.

Beweis auf cgroup-Ebene (CPU)
#

CG=$(sudo -u podman bash -c 'cd "$HOME" && podman inspect -f "{{ .State.CgroupPath }}" stressng')
cat "/sys/fs/cgroup${CG}/cpu.max"
# Beispiel: 50000 100000 (50ms von 100ms = 50%)

Memory-Limits testen
#

Unter cgroups v2 ist memory.max keine harte Kill-Grenze. Der Kernel bevorzugt Reclaim und Throttling. OOM-Kill ist die letzte Eskalation, wenn nichts anderes mehr funktioniert. Das macht es schwierig, einen OOM-Kill für Demo-Zwecke zu provozieren.

Stattdessen kannst du das Memory-Limit über die cgroup-Dateien und podman stats verifizieren:

# Terminal 1: Container mit Memory-Limit starten
sudo -u podman bash -c 'cd "$HOME" && podman run --rm \
  --name=stressng \
  --memory=64m \
  --memory-swap=64m \
  alpine \
  sh -c "apk add --no-cache stress-ng && stress-ng --vm 1 --vm-bytes 200M --timeout 30"'
# Terminal 2: Memory-Verbrauch beobachten
sudo -u podman bash -c 'cd "$HOME" && podman stats stressng'

Erwartung: MEM USAGE bleibt bei ~64 MB gedeckelt, obwohl stress-ng 200 MB anfordert.

Beweis auf cgroup-Ebene (Memory)
#

CG=$(sudo -u podman bash -c 'cd "$HOME" && podman inspect -f "{{ .State.CgroupPath }}" stressng')
cat "/sys/fs/cgroup${CG}/memory.max"
# Erwartet: 67108864 (64 MB in Bytes)

cat "/sys/fs/cgroup${CG}/memory.current"
# Zeigt aktuellen Verbrauch (nahe am Limit)

Best Practices für kleine Systeme
#

Empfohlene Container-Limits
#

sudo -u podman bash -c 'cd "$HOME" && podman run \
  --cpus=0.25 \
  --pids-limit=50 \
  --memory=100m \
  --memory-swap=100m \
  --security-opt no-new-privileges \
  alpine \
  nice -n 10 your-command'
OptionBeschreibung
--cpus=0.25Maximal 25% eines CPU-Kerns
--pids-limit=50Maximal 50 Prozesse/Threads
--memory=100mMaximal 100 MB RAM
--memory-swap=100mKein zusätzlicher Swap
--security-opt no-new-privilegesVerhindert Privilege Escalation
nice -n 10Niedrigere Prozess-Priorität

Merksätze
#

  • CPUWeight schützt Services
  • nice schützt das System
  • pids-limit schützt den Scheduler
  • no-new-privileges verhindert unbeabsichtigte Rechteausweitung

Fazit
#

Mit der head1328.podman Ansible-Rolle und den zusätzlichen Pre-Tasks ist der Raspberry Pi Zero 2 W bereit für rootless Container mit funktionierenden Ressourcen-Limits:

  • cgroup v2 mit allen Controllern aktiviert
  • Kernel-Parameter für Memory-Accounting gesetzt
  • systemd-Delegation für User-Container konfiguriert
  • Kritische System-Services vor CPU-Starvation geschützt
  • Dedizierter podman-User mit korrekten Berechtigungen

Ein Container, der trotz Memory-Limit nicht terminiert, ist kein Fehler; Linux verwaltet den Speicher korrekt und lagert bei Bedarf in den Swap aus.

Im nächsten Artikel installieren wir Pi-hole als Container auf dem vorbereiteten Raspberry Pi.

Verwendete Projekte
#

Eigene Rollen und Playbooks
#


Hat dir das hier geholfen?
Unterstütze gerne via:

Raspberry Pi Homelab - Dieser Artikel ist Teil einer Serie.
Teil 1: Dieser Artikel