2026-09-02 16:13:07 +02:00
2026-09-03 14:39:56 +02:00

DNF-Spiegelserver fuer Pakete ohne Flatpak

Ergaenzt den bestehenden Flathub-Filter-Spiegel um einen zweiten Weg fuer Anwendungen, die es nur als RPM gibt. Beide sollen unter einer Domain laufen:

  • https://<domain>/flatpak/... - bereits vorhanden
  • https://<domain>/dnf/... - dieser Stack

Funktionsprinzip

Der dnf-mirror-Container laeuft dauerhaft und fuehrt alle SYNC_INTERVAL_SECONDS (Default 6h) folgenden Zyklus aus:

  1. packages.yaml aktualisieren: git fetch/reset --hard gegen das in SOURCES_GIT_URL konfigurierte Git-Repo, das eine Datei packages.yaml enthaelt (siehe sources-repo-template/). Das passiert vor jedem Sync-Lauf - neue/geaenderte Eintraege in diesem separaten Repo wirken also spaetestens beim naechsten automatischen Lauf, ganz ohne Neubau/Neustart des Containers. packages.yaml listet Fremdrepos mit ihrer URL und genau den Paketnamen, die daraus geholt werden sollen, z.B.:
    repos:
      - id: vscode
        baseurl: https://packages.microsoft.com/yumrepos/vscode
        gpgkey: https://packages.microsoft.com/keys/microsoft.asc
        packages:
          - code
    

Danach, pro dort definiertem Fremdrepo, zunaechst in einen PRIVATEN, internen Zwischenspeicher (noch nicht oeffentlich sichtbar):

  1. dnf download holt aus dem Fremdrepo genau die unter packages: gelisteten Paketnamen (keine automatische Abhaengigkeitsaufloesung, keine sonstigen Pakete aus diesem Repo - siehe sources-repo-template/README.md).
  2. Filtern: Jedes heruntergeladene Paket wird trotzdem gegen die Paketnamen geprueft, die AlmaLinux selbst liefert (BaseOS/AppStream/ Extras/CRB/EPEL - konfigurierbar ueber ALMA_BASELINE_REPOIDS). Ist der Name dort vorhanden, wird das Paket verworfen: die Clients haben ihre Alma-Repos ja selbst schon eingebunden und loesen es von dort auf (Schutz gegen versehentliches Doppelspiegeln, falls jemand faelschlich ein Alma-Paket in packages.yaml eintraegt). Ausnahmen lassen sich im force_include-Feld von packages.yaml eintragen (z.B. wenn eine Anwendung eine neuere Version einer Bibliothek braucht als Alma mitbringt).
  3. Umsignieren: Die verbleibenden, tatsaechlich zu spiegelnden Pakete werden mit rpmsign --resign komplett neu signiert - die Original- Herstellersignatur wird ersetzt. Dadurch brauchen Clients nur einen GPG-Key (den des Spiegelservers), egal von wie vielen verschiedenen Herstellern die gespiegelten Pakete urspruenglich stammen.

Erst danach, EINMALIG ueber alle Fremdrepos zusammen:

  1. Zusammenfuehren: Die signierten Pakete aller Fremdrepos werden zu einem gemeinsamen, oeffentlichen Repo gemergt (rsync --delete ueber alle privaten Zwischenspeicher hinweg in /srv/dnf). Ein Fremdrepo, das in einem Sync-Lauf mal nicht erreichbar war, behaelt einfach seinen letzten erfolgreichen Stand im gemeinsamen Repo; wird ein Fremdrepo oder ein einzelnes Paket aus packages.yaml entfernt, verschwindet es beim naechsten Lauf auch aus dem gemeinsamen Repo.
  2. Metadaten bauen: createrepo_c fuer die eigentlichen Repo-Metadaten des gemeinsamen Repos, dazu AppStream-Katalogdaten (appstreamcli compose, siehe unten), damit die Anwendungen mit Name/Icon/Beschreibung in Discover (GNOME Software) auftauchen statt nur per dnf/pkcon auf der Kommandozeile sichtbar zu sein.
  3. repomd.xml wird zusaetzlich separat signiert (repomd.xml.asc), damit Clients repo_gpgcheck=1 nutzen koennen (schuetzt auch die Metadaten selbst, nicht nur die einzelnen Pakete).
  4. Eine fertige Client-.repo-Datei wird nach /srv/dnf/$MIRROR_REPO_ID.repo geschrieben und ist unter https://<domain>/dnf/$MIRROR_REPO_ID.repo abrufbar - das ist das einzige Repo, das Clients einbinden muessen.

Der nginx-Container bekommt das Ausgabe-Volume read-only gemountet und liefert es unter /dnf/ aus. Er baut keine Pakete, sondern ist nur fuer den Zugriff von aussen zustaendig.

Erste Einrichtung

# 1. Paketliste: sources-repo-template/packages.yaml (angepasst) in ein
#    EIGENES, separates Git-Repo pushen (siehe sources-repo-template/README.md)
cp sources-repo-template/packages.yaml /pfad/zum/eigenen/git-repo/packages.yaml
# Inhalt pruefen/anpassen (eigene Fremdrepos + Paketnamen eintragen), dann
# committen+pushen ins neue Git-Repo

# 2. Compose-Stack konfigurieren
cp .env.example .env
# .env anpassen: mindestens MIRROR_DOMAIN, MIRROR_REPO_ID, GPG_NAME, GPG_EMAIL,
# SOURCES_GIT_URL

# 3. Bei SSH-Zugriff aufs Git-Repo: Deploy-Key (nur Lesezugriff) ablegen
cp /pfad/zum/deploy-key ./git-ssh/id_ed25519
chmod 600 ./git-ssh/id_ed25519

docker compose up -d --build
docker compose logs -f dnf-mirror   # ersten Sync-Lauf beobachten

Der ./gpg-Ordner neben der docker-compose.yml wird beim ersten Start vom Container selbst befuellt (GPG-Schluesselpaar) - dort ist nichts manuell vorzubereiten, aber der Ordner sollte auf dem Host entsprechend geschuetzt (nicht world-readable) und gesichert/gebackupt werden: geht er verloren, muessen alle Clients den neuen Public Key neu importieren.

Nach dem ersten Lauf:

  • https://<domain>/dnf/RPM-GPG-KEY-mirror - der Public Key des Spiegels
  • https://<domain>/dnf/company-mirror.repo (bzw. $MIRROR_REPO_ID.repo) - die eine Repo-Datei, die Clients einbinden
  • https://<domain>/dnf/ - das gemeinsame Repository selbst, enthaelt die Pakete aus allen in packages.yaml konfigurierten Fremdrepos

Neue Anwendungen spiegeln

Fuer jede Anwendung ohne Flatpak einen Eintrag unter repos: in der packages.yaml im separaten Paketlisten-Git-Repo ergaenzen (Fremdrepo- URL + die gewuenschten Paketnamen unter packages:), committen, pushen - der Container zieht das beim naechsten Sync-Lauf automatisch. Kein Zugriff auf den Docker-Host/Compose-Stack noetig, das kann also z.B. per Merge-Request- Review an ein anderes Team delegiert werden.

Clients anbinden

Einmalig (per Ansible/Kickstart/Intune-Skript/Image-Build - vermutlich schon Teil eurer bestehenden Entra-ID-Provisioning-Pipeline), z.B. mit dem mitgelieferten Skript:

./client/bootstrap.sh software.example.org

Das importiert den Mirror-GPG-Key und legt die eine .repo-Datei ab - Clients sehen darin von Anfang an alle gespiegelten Anwendungen aus allen konfigurierten Fremdrepos, ganz ohne pro Anwendung ein weiteres Repo einbinden zu muessen. Rootlose Installation/Update/Deinstallation laeuft danach ueber die bereits bestehenden Polkit-Regeln fuer PackageKit wie gewohnt - hier ist nichts zusaetzlich noetig.

Bekannte Einschraenkungen

  • AppStream/Discover: appstreamcli compose extrahiert AppStream-Metadaten aus Paketen, die selbst .metainfo.xml/.desktop-Dateien mitbringen. Bringt eine Anwendung so etwas nicht mit, erscheint sie trotzdem installierbar, aber ohne Katalogeintrag (Name/Icon/Beschreibung) in Discover - nur per Suche nach dem Paketnamen. Das ist eine Eigenschaft des Quellpakets, keine Konfigurationsluecke.
    • Der Stack ist auf AlmaLinux 10 und neuer ausgelegt. Gebaut wird der Katalog mit appstreamcli compose (Pakete appstream + appstream-compose - Letzteres ist ein eigenes Subpaket, ohne das appstreamcli compose zur Laufzeit mit "AppStream Compose binary ... was not found" scheitert). Da dieses Kommando auf einem Verzeichnisbaum statt auf .rpm-Dateien arbeitet, entpackt sync.sh vorher die relevanten Pfade (usr/share/{applications,metainfo,icons,pixmaps}) aus allen Paketen in so einen Baum.
    • Format der beiden repodata-Eintraege ist heikel. Auf dem Client holt PackageKit die repodata-Typen appstream und appstream-icons und reicht sie an as_utils_install_metadata_file() von libappstream weiter, das sie nach /var/cache/swcatalog/{xml,icons}/ installiert - erst von dort lesen Discover und appstreamcli. Diese Funktion ist bei Dateinamen sehr waehlerisch, und ein Fehlschlag laesst den kompletten Repo-Refresh scheitern (nicht nur die Icons). Deshalb schreibt sync.sh beides ausdruecklich mit modifyrepo_c --compress-type=gz:
      • Der Katalog muss auf .xml.gz enden. modifyrepo_c komprimiert die uebergebene Datei neu und nutzt auf AlmaLinux 10 per Default zstd; das dort installierte AppStream 1.0.2 kennt .xml.zst aber noch nicht (erst ab 1.0.5) und lehnt die Datei mit "Can not process files of this type" ab.
      • Der Icon-Tarball muss auf .tar.gz enden und eine bekannte Groesse (48x48, 64x64, 128x128, jeweils optional @2) im Dateinamen tragen, sonst gibt es "Unable to find valid icon size in icon tarball name". Er wird flach nach /var/cache/swcatalog/icons/<repo-id>/<groesse>/ entpackt, darf also nur die PNGs auf oberster Ebene enthalten. PackageKit holt genau einen appstream-icons-Eintrag, sync.sh liefert deshalb genau eine Groesse aus (bevorzugt 64x64).
      • appstreamcli compose schreibt den Origin nur in Datei- und Verzeichnisnamen, nicht als origin=-Attribut in den Katalog. libappstream liest ihn aber genau daraus und findet ohne ihn die Icons nicht - sync.sh traegt ihn deshalb nachtraeglich ein.
    • jpackage-basierte Java/JavaFX-Apps (u.a. XPipe): Diese legen ihre .desktop-Datei typischerweise NICHT unter /usr/share/applications/ ab, sondern direkt im eigenen Installationsverzeichnis (z.B. /opt/xpipe/xpipe.desktop) und verschieben sie erst per %post-Skript dorthin - das sieht sync.sh beim reinen Lesen des Pakets nicht. Der sync.sh durchsucht deshalb zusaetzlich das gesamte Paket nach .desktop/.metainfo.xml/.appdata.xml-Dateien ausserhalb der Standardpfade und holt genau diese gezielt nach.
    • Reine Kommandozeilen-Pakete (dotnet-sdk, terraform, kubectl, ...) bringen weder .desktop- noch .metainfo.xml-Dateien mit und sind in Discover deshalb standardmaessig gar nicht auffindbar - auch nicht ueber den exakten Paketnamen, denn Discovers PackageKit-Backend durchsucht ausschliesslich den AppStream-Pool (per dnf/pkcon bleiben sie natuerlich installierbar). Wer solche Pakete im Katalog haben will, traegt ihren Paketnamen in console_apps: der packages.yaml ein; sync.sh erzeugt dann eine console-application-Komponente aus dem RPM-Header (%{SUMMARY}, %{DESCRIPTION}, %{URL}, %{LICENSE}) plus <provides><binary> fuer die Befehle aus /usr/[s]bin. Details siehe sources-repo-template/README.md. Bewusst opt-in, damit nicht saemtliche Runtime- und Bibliotheks-Unterpakete als "Anwendungen" im Katalog landen.
    • Fehlende .metainfo.xml + <pkgname>: Hat eine Anwendung nur eine .desktop-Datei, aber keine .metainfo.xml (der Normalfall bei unabhaengigen Drittanbieter-RPMs), erzeugt sync.sh selbst eine minimale Ersatz-Metainfo daraus (Name/Beschreibung/Icon/Kategorien aus der .desktop-Datei). appstreamcli compose (AppStream 1.0+) verlangt dafuer zwingend eine <description>, sonst schlaegt die betroffene Komponente mit description-missing fehl. Ausserdem braucht JEDE Komponente - auch eine echte, vom Hersteller mitgelieferte .metainfo.xml - ein <pkgname>-Element, damit PackageKit sie einem installierbaren RPM zuordnen kann; Upstream-Metainfo-Dateien haben das so gut wie nie (kennen den Distro-Paketnamen nicht), sync.sh ergaenzt es deshalb automatisch beim Einsammeln der Dateien.
    • Ist keins von beiden installiert, schlaegt die Erzeugung fehl, oder bringt das Paket auch an keinem dieser Orte eine .desktop-/AppStream- Datei mit, bleiben Pakete ganz normal per dnf/pkcon installierbar, erscheinen aber ohne Katalogeintrag in Discover (siehe Log-Warnung).
  • PackageKit-Cache auf dem Client: dnf makecache aktualisiert nur den Cache des interaktiven dnf-Kommandos. PackageKit (und damit Discover) haelt einen davon getrennten eigenen Metadaten-Cache und aktualisiert ihn per Default nicht zuverlaessig von selbst zeitnah. client/bootstrap.sh ruft deshalb zusaetzlich pkcon refresh -c 1 auf - ohne das kann ein neu eingebundenes Repo (oder neu hinzugekommene Pakete darin) in Discover trotz korrekter AppStream-Daten verzoegert oder gar nicht auftauchen.
  • Namensbasierter Abgleich: Paket X wird uebersprungen, sobald ein Paket gleichen Namens in Alma existiert - unabhaengig von der Version. Falls eine gespiegelte Anwendung eine neuere Version einer Alma-Bibliothek als Abhaengigkeit braucht, dafuer force_include in packages.yaml nutzen.
  • Keine Abhaengigkeitsaufloesung: packages.yaml listet die zu spiegelnden Pakete explizit; dnf download holt genau diese Namen aus dem jeweiligen Fremdrepo, ohne fehlende Abhaengigkeiten automatisch nachzuladen. Fehlt einer gespiegelten Anwendung eine Bibliothek, die weder aus Alma/EPEL noch aus demselben Fremdrepo kommt, muss sie explizit mit in packages: aufgenommen werden.
  • Ein gemeinsames Repo, ein gemeinsamer Namensraum: Alle Fremdrepos aus packages.yaml werden zu einem einzigen Repo zusammengefuehrt. Zwei verschiedene Fremdrepos mit einem gleichnamigen Paket (gleicher .rpm-Dateiname) landen deshalb im selben Speicherplatz - das zuletzt gesyncte gewinnt. Bei der Zusammenstellung von packages.yaml also darauf achten, dass sich die gewaehlten Anwendungen nicht ueberschneiden.
  • GPG-Key-Ablage: Der Signing-Key liegt (statt in einem anonymen Docker- Volume) im Ordner ./gpg direkt neben der docker-compose.yml - dadurch laesst er sich mit normalen Host-Mitteln sichern/backupen/einschraenken. Der Container startet dafuer intern kurz als root, chownt ./gpg auf den unprivilegierten mirror-User und wechselt dann per setpriv dauerhaft zu diesem (siehe mirror/entrypoint.sh) - der eigentliche Sync inkl. git/ dnf/gpg laeuft nie als root.
  • GPG-Passphrase: Der Signing-Key wird standardmaessig ohne Passphrase erzeugt, damit der Container vollautomatisch signieren kann. Der Schutz liegt dann in den Dateirechten von ./gpg (nicht world-readable lassen!). Eine Passphrase ist in run-as-mirror.sh fuer die Schluesselerzeugung vorbereitet, automatisches Signieren mit passphrasegeschuetztem Key ist aber NICHT fertig verdrahtet (braucht zusaetzlich gpg-preset-passphrase o.ae.) - fuer den unbeaufsichtigten Betrieb wird die leere Passphrase empfohlen.
  • Git-Zugriff: SOURCES_GIT_URL wird vor jedem Sync-Lauf gefetcht. Schlaegt das fehl (Netzwerk, Auth), laeuft der Sync mit der letzten erfolgreich geholten Paketliste weiter (nicht beim allerersten Lauf - dort bricht der Sync-Zyklus ohne erreichbares Git-Repo komplett ab). Nur ein Lesezugriff (Deploy-Key/Read-only-Token) ist noetig.
  • TLS: Der nginx-Service ist bewusst schlank gehalten (Port 80). Fuer TLS entweder Zertifikate mounten und einen listen 443 ssl Server-Block ergaenzen, oder - passend zum Wunsch "eine Domain fuer beide Repos" - diesen Stack hinter einem bereits vorhandenen Reverse Proxy/Loadbalancer betreiben, der auch den Flatpak-Spiegel terminiert.
  • Upgrade von einer aelteren Version dieses Stacks (mit einem Repo PRO Fremdrepo unter /dnf/<id>/): Das Ausgabeformat hat sich grundlegend geaendert (jetzt ein gemeinsames Repo direkt unter /dnf/). Vor dem ersten Start mit dieser Version das dnf-repo-data-Volume einmalig leeren (docker compose down + Volume-Inhalt loeschen), damit keine alten <id>/-Verzeichnisse und <id>.repo-Dateien liegen bleiben, und anschliessend allen Clients die neue, eine .repo-Datei ausrollen.
  • Dieser Aufbau wurde nicht gegen echte Internet-Upstreams end-to-end getestet (keine Docker-Umgebung/Internetzugriff in dieser Session verfuegbar) - Skripte sind syntaktisch geprueft, der erste docker compose up sollte aber mit echten Repo-Definitionen beobachtet werden.

Verzeichnisstruktur

docker-compose.yml
.env.example
gpg/                    (leer, wird vom Container befuellt) - Signing-Key,
                         auf dem Host sichern/schuetzen
git-ssh/                 optional: SSH-Deploy-Key fuers Paketlisten-Git-Repo
mirror/
  Dockerfile             AlmaLinux-10-Image mit dnf-plugins-core, createrepo_c,
                         rpm-sign, appstream+appstream-compose,
                         git, python3-pyyaml, jq
  entrypoint.sh           laeuft als root: chownt ./gpg, wechselt per setpriv
                         zu "mirror"
  run-as-mirror.sh        laeuft als "mirror": GPG-Key-Erzeugung/-Export,
                         Sync-Schleife
  sync.sh                Kernlogik: packages.yaml aus Git holen -> je
                         Fremdrepo dnf download -> filtern -> signieren
                         (privater Cache) -> alle Fremdrepos zu EINEM Repo
                         zusammenfuehren -> Metadaten -> Client-.repo
  config/
    alma-baseline.repo.d/  optional: eigene Baseline-Repos fuer den Abgleich
sources-repo-template/   Vorlage fuer das SEPARATE Git-Repo mit der
                         Paketliste (packages.yaml)
nginx/
  conf.d/repo.conf        liefert /dnf/... aus, Platzhalter fuer /flatpak/...
client/
  bootstrap.sh            einmaliges Client-Onboarding (Key-Import + .repo)
S
Description
No description provided
Readme
289 KiB
Languages
Shell 93.5%
Dockerfile 6.5%