Dieser Post ist Teil der Serie über mein privates BeagleBone-Black-Projekt. Die CI/CD-Infrastruktur des Projekts läuft auf Drone CI — aber nicht mit Docker, sondern mit Podman als Container-Backend. Was auf dem Papier einfach klingt, hat in der Praxis ein paar Ecken.

Warum Podman statt Docker?

Die kurze Antwort: rootless-Betrieb und kein Daemon.

Docker erfordert einen privilegierten Daemon der permanent läuft — dockerd — und der Zugriff auf den Docker-Socket bedeutet de facto Root-Rechte auf dem Host. Das ist für viele Umgebungen ein akzeptables Tradeoff, für meinen privaten Server nicht.

Podman läuft ohne Daemon:

# Docker: Client spricht mit Daemon der als root läuft
docker run ubuntu echo hello

# Podman: kein Daemon, direkter fork/exec als normaler User
podman run ubuntu echo hello

Konkrete Vorteile im Betrieb:

  • Kein dauerhaft laufender privilegierter Prozess

  • Container laufen unter dem eigenen UID des aufrufenden Users

  • podman-auto-update statt Docker Watchers

  • Drop-in-kompatibel zu Docker für die meisten Fälle

Podman ist kein perfekter Docker-Ersatz. Wo es Unterschiede gibt, beschreibe ich sie weiter unten bei den Fallstricken.

Drone CI Serverinstallation mit Podman

Drone Server und Runner laufen selbst als Container — gestartet mit Podman.

Drone Server

podman run \
  --detach \
  --name=drone \
  --restart=always \
  --env=DRONE_GITEA_SERVER=https://gitea.example.com \
  --env=DRONE_GITEA_CLIENT_ID=<client-id> \
  --env=DRONE_GITEA_CLIENT_SECRET=<secret> \
  --env=DRONE_RPC_SECRET=<shared-secret> \
  --env=DRONE_SERVER_HOST=drone.example.com \
  --env=DRONE_SERVER_PROTO=https \
  --publish=80:80 \
  --publish=443:443 \
  --volume=/var/lib/drone:/data \
  docker.io/drone/drone:2

Drone Runner (Podman-Backend)

Der Standard-Drone-Docker-Runner spricht gegen den Docker-Socket. Für Podman gibt es zwei Wege:

Option A — Docker-kompatiblen Socket exponieren:

Podman kann einen Docker-kompatiblen Socket bereitstellen:

systemctl --user enable --now podman.socket
# Socket liegt unter: /run/user/<UID>/podman/podman.sock

Den Runner dann mit dem Podman-Socket starten:

podman run \
  --detach \
  --name=drone-runner \
  --restart=always \
  --env=DRONE_RPC_PROTO=https \
  --env=DRONE_RPC_HOST=drone.example.com \
  --env=DRONE_RPC_SECRET=<shared-secret> \
  --env=DRONE_RUNNER_CAPACITY=2 \
  --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock \
  docker.io/drone/drone-runner-docker:1

Option B — Exec Runner (kein Container-Socket nötig):

Der Exec Runner führt Steps direkt auf dem Host aus — einfacher, weniger isoliert:

podman run \
  --detach \
  --name=drone-runner-exec \
  --restart=always \
  --env=DRONE_RPC_PROTO=https \
  --env=DRONE_RPC_HOST=drone.example.com \
  --env=DRONE_RPC_SECRET=<shared-secret> \
  --volume=/var/run/drone-runner:/data \
  docker.io/drone/drone-runner-exec:latest

Ich verwende Option A weil der Docker-Runner die bessere Step-Isolation bietet und .drone.yml-Dateien ohne Anpassung funktionieren.

Fallstricke

Volume-Mounts und Dateiberechtigungen

Das größte Problem beim rootless Betrieb: Dateiberechtigungen.

Rootless Podman mappt UIDs im Container auf Subuid-Ranges des Host-Users. Ein Prozess der im Container als root (UID 0) läuft, läuft auf dem Host als UID des Users plus Offset.

Problem: Ein Build-Step schreibt Dateien unter root im Container — nach dem Build gehören diese Dateien auf dem Host einem UID der nicht existiert.

Lösung: Explizites UID-Mapping oder --userns=keep-id:

# .drone.yml
steps:
  - name: build
    image: ubuntu:22.04
    volumes:
      - name: workspace
        path: /workspace
    environment:
      DRONE_WORKSPACE_BASE: /workspace

volumes:
  - name: workspace
    host:
      path: /tmp/drone-workspace

Oder den Drone-Runner mit --userns=keep-id starten, damit der Container-User mit dem Host-User übereinstimmt.

Netzwerk-Isolation zwischen Steps

Docker-Runner-Steps im selben Pipeline-Run teilen ein Netzwerk. Bei Podman mit rootless-Setup kann das Netzwerk-Backend (slirp4netns vs pasta) zu unterschiedlichen Ergebnissen führen.

Problem: Ein Datenbankcontainer in Step A ist aus Step B unter localhost nicht erreichbar.

Lösung: Service-Container explizit benennen und per Hostname ansprechen:

services:
  - name: postgres
    image: postgres:15
    environment:
      POSTGRES_DB: testdb
      POSTGRES_PASSWORD: test

steps:
  - name: test
    image: golang:1.21
    commands:
      # Hostname ist der Service-Name, nicht localhost
      - go test -db-host=postgres ./...

Secrets und Environment-Variablen

Drone-Secrets funktionieren unverändert — das ist kein Podman-spezifisches Problem. Aber: bei rootless Podman dürfen Secrets nicht in gemountete Host-Verzeichnisse geschrieben werden die anderen Users zugänglich sind.

Best Practice: Secrets nur als Umgebungsvariablen, nie als Dateien in /tmp.

steps:
  - name: deploy
    image: alpine
    environment:
      SSH_KEY:
        from_secret: ssh_private_key
    commands:
      # Key in memory halten, nie auf Disk
      - eval $(ssh-agent -s)
      - echo "$SSH_KEY" | ssh-add -
      - ssh user@host "systemctl restart myservice"

Cross-Compilation und privilegierte Operationen

Mein Build-Setup cross-kompiliert für ARMv7. Der Cross-Kompilierungs-Container braucht keine besonderen Rechte — aber ich bin anfangs in die Falle getappt, --privileged zu setzen weil ein anderer Schritt es brauchte.

Regel: Jeden Step auf minimale Rechte reduzieren. --privileged nur wenn wirklich nötig (z. B. Kernel-Module laden). Cross-Kompilierung braucht es nie.

Funktionierende .drone.yml-Konfiguration

Das ist meine tatsächliche Pipeline — vereinfacht aber funktionierend:

kind: pipeline
type: docker
name: beaglebone-build

steps:
  - name: build-c
    image: ubuntu:22.04
    commands:
      - apt-get update -q && apt-get install -y cmake gcc-arm-linux-gnueabihf
      - cmake -DCMAKE_TOOLCHAIN_FILE=cmake/armv7-toolchain.cmake -B build-arm
      - cmake --build build-arm

  - name: build-rust
    image: rust:1.75
    commands:
      - rustup target add armv7-unknown-linux-gnueabihf
      - cd rust
      - cargo build --release --target armv7-unknown-linux-gnueabihf

  - name: build-go
    image: golang:1.21
    commands:
      - cd go-api
      - go build ./...
      - go test ./...

  - name: deploy
    image: alpine
    environment:
      SSH_KEY:
        from_secret: bbb_ssh_key
    commands:
      - apk add --no-cache openssh-client rsync
      - eval $(ssh-agent -s)
      - echo "$SSH_KEY" | ssh-add -
      - rsync -avz build-arm/ pi@beaglebone:/opt/bbb-hal/
      - ssh pi@beaglebone "systemctl restart bbb-hal"
    when:
      branch:
        - main

Fazit

Podman als Drone-CI-Backend funktioniert gut — mit ein paar Anpassungen.

Wann Podman sich lohnt:

  • Kein Docker-Daemon soll dauerhaft laufen

  • Rootless-Betrieb ist eine Anforderung

  • Privater Server ohne komplexe Container-Orchestrierung

Wann Docker einfacher ist:

  • Große Teams mit bestehenden Docker-Workflows

  • Viele Images die Docker-Socket direkt nutzen

  • Wenn Kompatibilitätsprobleme mehr Zeit kosten als der Daemon-Betrieb

Für mein privates Projekt ist Podman die richtige Wahl. Die Fallstricke waren lösbar, und das Ergebnis ist eine CI-Infrastruktur die ohne privilegierte Hintergrunddienste auskommt.


Nächster Post in der Serie: Cross-Kompilierung für ARMv7 mit CMake