← Übersicht  ·  Skripte & Dateien  ·  cloud-host · Runbook

Jenkins lokal aus dem Hetzner-Snapshot

Der geteilte CI-Jenkins für die Bibliothek-Enterprise-Pipeline lief auf einem Hetzner-Server. Er läuft jetzt lokal im shared-infra-Compose – mit vollem Job- und Credential-Stand aus dem letzten Snapshot. Dieses Runbook hält fest, wie der Umzug lief, wo es klemmte und wie der Alltag jetzt aussieht.

Windows 10 · podman 6.0.2 shared-infra Compose (podman-compose 1.6.0) Stand: 1. September 2026

01 Wo alles steht

Der Ist-Zustand nach dem Umzug – damit die restlichen Abschnitte konkret sind.

JenkinsLokal als Container shared-jenkins im shared-infra-Compose. UI http://localhost:8080, Login admin / admin.
Hetzner-ServerAus. Snapshot 426664299 (~4–6 GB, ~0,10 €/Mon) bleibt. Zurück mit .\scripts\jenkins.ps1 up.
OKD-Zielokd-sno, Namespace library. Seit dem Tailscale-Umbau ist die OKD-API (6443) nur noch übers Tailnet erreichbar – der shared-jenkins-Container erbt den hosts-Eintrag des Laptops (100.67.147.8  api.sno.<okd-ip>.nip.io) und kommt darüber an den Cluster.
Die IP-Beispiele unten (62.238.124.7) sind der Stand zum Zeitpunkt dieses Laufs – deine steht in .\scripts\sno.ps1 status und wechselt bei jedem sno.ps1 install.
Letzte Deploys#20 grün (19,5 min, kalt) · #21 rot (Registry-Blip) · #22 grün (2,5 min, warm).
Wiederhergestellt4 Jobs, Build-Historie #1–17, Credentials github-library-deploykey / KUBECONFIG_PROD / okd-registry.

02 Warum lokal

Ein zweiter Server für ein Projekt mit gelegentlichen Deploys ist Overhead.

Auf Hetzner lief ein cpx42 (~0,10 €/h) plus die Firewall-Choreografie, die der Pipeline Zugriff auf die OKD-API verschafft (jenkins.ps1 trägt die Server-IP bei jedem up in okd-sno-fw:6443 ein und bei down wieder aus). Für den aktuellen Bedarf reicht ein Jenkins in der ohnehin laufenden podman-machine.

Der Hetzner-Weg bleibt erhalten: jenkins.ps1 up baut den Server aus dem Snapshot in ~95 s wieder auf. Der Snapshot ist die Rückfahrkarte, jenkins.ps1 destroy verbrennt sie.

Mitgekommen ist alles

Der komplette „25 Fallen“-Apparat aus der OKD-Deploy-Anleitung steckt als init.groovy.d/-Skripte im jenkins_home und läuft bei jedem Boot idempotent: 05-git-hostkey.groovy (GIT_SSH_COMMAND), 10-okd-creds.groovy (legt KUBECONFIG_PROD + okd-registry aus okd.kubeconfig + okd.token neu an), 21-job-scm.groovy (biegt den Job auf */okd-deploy + Deploy-Key um, hängt DEPLOY_ONLY an).

03 Wie der Stand rüberkam

„Weg B“: den laufenden Stand aus dem Server ziehen statt neu aufzusetzen. Nachbaubar, falls es nochmal nötig wird.

  1. Container stoppen für eine konsistente Kopie.
ssh · hetzner
ssh root@<jenkins-ip> "podman stop -t 30 jenkins"
  1. /opt/jenkins-home als Tar ziehen – 750 MB roh, 333 MB gepackt.
ssh · hetzner
ssh root@<jenkins-ip> "tar -C /opt/jenkins-home --numeric-owner -czf - ." \
  > jenkins-home.tar.gz
  1. jenkins.ps1 down – Snapshot 426664299, Server gelöscht, die jenkins-auto-Regel in okd-sno-fw entfernt.
  2. Tar ins Volume shared-infra_jenkins-data entpacken über einen Helfer-Container – altes lokales jenkins_home vorher weg.
helfer-container
find /dest -mindepth 1 -delete
tar -C /dest -xzf /src/jenkins-home.tar.gz
chown -R 1000:1000 /dest
  1. Starten.
powershell
cd shared-infra
podman compose up -d jenkins

Ergebnis: 4 Jobs, Build-Historie library-platform-pipeline #1–17, alle drei Credentials. credentials.xml + secret.key + secrets/ kommen im Tar mit, deshalb bleiben die verschlüsselten Credentials benutzbar.

04 Der podman-compose-Trick

Der einzige echte Umbau: den Podman-Socket der podman-machine in den Jenkins-Container bekommen, damit die podman build-Stufen laufen.

podman compose nutzt hier podman-compose 1.6.0 (Python, via scoop). Das schreibt unter Windows in assert_volume() jeden absoluten Bind-Mount-Pfad über os.path.abspath(os.path.join(basedir, src)) auf C:\… um – auch die Langform mit type: bind – und legt dort per os.makedirs leere Ordner an.

Bind-Mount – kurz oder lang

Quelle /run/user/1000/podman/podman.sock → podman-compose → im Container /mnt/c/run/user/1000/podman/podman.sock. Existiert nicht in der VM, Socket tot, leerer Ordner auf C:\.

Benanntes Volume mit bind-Backing

driver_opts: { o: bind, device: /run/user/1000/podman } geht unverändert an podman volume create und wird von podman in der VM aufgelöst. Echter Socket im Container.

Gebunden wird das Verzeichnis /run/user/1000/podman – die Socket-Datei selbst lässt sich nicht auf das _data-Dir des Volumes mounten. Gemountet nach /run/podman-host. Ein zweiter Mount desselben Volumes auf /var/run/docker.sock verdrängt den kaputten Basis-Mount aus docker-compose.yml.

shared-infra/docker-compose.override.yml
volumes:
  podman-sock:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /run/user/1000/podman

services:
  jenkins:
    ports:
      - "8080:8080"          # UI direkt - Traefik bindet rootless kein :80
      - "50000:50000"
    restart: unless-stopped
    environment:
      CONTAINER_HOST: unix:///run/podman-host/podman.sock
      DOCKER_HOST:    unix:///run/podman-host/podman.sock
    volumes:
      - podman-sock:/run/podman-host
      - podman-sock:/var/run/docker.sock   # verdraengt den Basis-Mount
Verifiziert

podman build aus dem Jenkins-Container gegen die Host-Engine läuft, und die Verdrahtung übersteht podman machine stop / start.

05 Dateien & Maschine

Was neu ist – und was in der podman-machine gesetzt werden musste.

Neu in shared-infra/ (dort kein Git-Repo)

.envNur Doku-Kommentar. CONTAINER_ENGINE_SOCKET bleibt bewusst ungesetzt – der Socket kommt über das Volume oben.
docker-compose.override.ymlWird von podman compose automatisch mitgeladen: Host-Port 8080, restart: unless-stopped, CONTAINER_HOST/DOCKER_HOST, das podman-sock-Volume.

Einmalig in der podman-machine

Per podman machine ssh. Verschwindet bei podman machine rm und muss dann neu gesetzt werden.

podman machine ssh
# shared-jenkins nach `podman machine start` selbst hochfahren
sudo systemctl enable podman-restart.service
sudo chown -R user:user ~/.config/systemd
systemctl --user enable podman-restart.service

# `podman build -v` loest die -v-Quelle in der VM auf
sudo mkdir -p /opt/m2cache && sudo chmod 777 /opt/m2cache

Und einmalig im Container, weil workspace/ mit kaputtem @script/.git im Tar lag:

bash
podman exec shared-jenkins rm -rf /var/jenkins_home/workspace/*

podman-machine ist größer als sie aussieht. podman machine list zeigt „2 GiB“ – das ist nominell, WSL2 vergibt RAM dynamisch (real ~16 GB / 8 CPU). podman machine set --cpus/--memory geht bei WSL-Maschinen nicht.

06 Täglicher Betrieb

Start, Stop, und der Weg zurück auf Hetzner.

start / stop
# nach `podman machine start` kommt Jenkins per podman-restart.service selbst;
# explizit:
cd shared-infra && podman compose up -d jenkins

# stoppen - Daten bleiben im Volume jenkins-data
podman compose stop jenkins
  • UI: http://localhost:8080admin / admin. CASC setzt das Passwort bei jedem Boot neu aus JENKINS_ADMIN_PASSWORD, egal was im wiederhergestellten users/ steht.
  • Zurück auf Hetzner: .\scripts\jenkins.ps1 up – Server aus Snapshot, gleiche Firewall-Automatik.
  • Snapshot endgültig weg: .\scripts\jenkins.ps1 destroy.

07 Deploy auslösen

DEPLOY_ONLY=true überspringt Test-, Lint- und Frontend-Stufen und geht direkt Build → Push in die OKD-Registry → helm upgrade.

Der Crumb muss über einen Cookie-Jar mit – sonst HTTP 403 (Falle 11 der OKD-Deploy-Anleitung).

bash
J='http://admin:admin@localhost:8080'
CRUMB=$(curl -s -c cj "$J/crumbIssuer/api/json" \
  | python -c "import sys,json;print(json.load(sys.stdin)['crumb'])")

curl -s -b cj -H "Jenkins-Crumb:$CRUMB" -X POST \
  "$J/job/library-platform-pipeline/buildWithParameters" \
  --data-urlencode "DEPLOY_ONLY=true"

Oder in der UI: library-platform-pipeline → „Build mit Parametern“. OKD muss laufen (sno.ps1 up).

Voraussetzung: Tailnet

Die Deploy-Stufen reden mit der OKD-API auf 6443 – seit dem Tailscale-Umbau tailnet-only. Der Laptop braucht den hosts-Eintrag <tailscale-ip>  api.sno.<okd-ip>.nip.io und eine aktive Tailscale-Verbindung. Der shared-jenkins-Container erbt beides über die podman-machine (geprüft: kubectl get ns library aus dem Container löst den Namen auf die Tailnet-IP auf und antwortet). Die Image-Pushes gehen dagegen an die Registry-Route auf 80/443 – die bleibt öffentlich.

08 Stolpersteine (#18–#22)

Was in den ersten lokalen Läufen hochkam – und der Fix.

BuildSymptomUrsacheFix
#18 Checkout: fatal: not in a git directory workspace/ aus dem Tar mit kaputtem @script/.git rm -rf /var/jenkins_home/workspace/*
#19 podman build: validating volumes: faccessat /opt/m2cache: no such file or directory -v-Quelle wird gegen die VM aufgelöst, dort fehlte sie mkdir -p /opt/m2cache && chmod 777 in der podman-machine
#21 podman logindial tcp 62.238.124.7:80: connection refused openshift-apiserver auf dem Single-Node kurz neu → Route-API weg → Ingress-Router NotReady → *.apps.sno… komplett offline selbstheilend in 1–2 min, dann neu triggern
#22 einzelner helm upgradecluster unreachable: i/o timeout kurzer 6443-Blip, Pipeline fiel für den Service auf helm template-Dry-Run zurück nicht-fatal – Service lag ohnehin schon auf dem Tag
Traefik / jenkins.localhost reagiert nicht rootless podman bindet Port 80 nicht Override gibt Jenkins direkt :8080, Traefik bleibt außen vor
Muster

#21 und der Blip in #22 sind SNO-Fragilität unter Last, nicht das lokale Setup: ein winziger Node trägt OKD-Plattform + Monitoring + 10 Services + Kafka/Keycloak/Postgres, und helm --wait plus Image-Pushes treiben etcd/apiserver an die Grenze. Sauberer Fix wäre ein retry-Block auf der Push-/Deploy-Stage im Jenkinsfile des Ziel-Repos.

09 Verifikation

Zwei komplette Läufe vom lokalen Jenkins nach OKD.

Build #20SUCCESS, ~19,5 min (kalt). 10 Images gebaut → OKD-interne Registry → 9× helm upgrade --install … --wait --timeout 3m, alle STATUS: deployed.
Build #22SUCCESS, 145 s (m2cache und Image-Layer warm). Ein 6443-Blip bei fine-service → Dry-Run-Fallback, Rest deployte.
Endstandlibrary.apps.sno.62.238.124.7.nip.io/actuator/health{"status":"UP"} · Frontend HTTP 200 · 9/9 Library-Pods ready · alle Deployments auf Tag 597452ba.

+ Was noch aussteht

  • Jenkinsfile: retry-Block auf Push- und Deploy-Stage, um die SNO-Blips abzufangen – Änderung im Ziel-Repo LAB-WIP-MAVEN-Bibliothek-Enterprise.
  • okd.kubeconfig / okd.token im jenkins_home sind auf die OKD-IP zum Snapshot-Zeitpunkt gebacken. Nach einem sno.ps1 install (neue IP) erneuern – 10-okd-creds.groovy legt die Credentials daraus bei jedem Boot neu an.
  • Traefik lokal ungelöst (rootless kein Port 80). Der direkte :8080 reicht für den Jenkins-Betrieb.

+ Bezug zu den Skripten

scripts/jenkins.ps1up / down / destroy für den Hetzner-Server. down zieht den Snapshot, den dieses Runbook auspackt.
shared-infra/docker-compose.ymlDer geteilte Stack (Traefik, Jenkins, optional Postgres/Keycloak/Kafka/Observability). Die .override.yml daneben ist lokal, unversioniert.
shared-infra/jenkins/Dockerfile + casc.yaml + plugins.txt – das Jenkins-Image (Maven 3.9.9, Node 20, Helm, kubectl, Chromium). Baut lokal wie auf dem Server.