<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Code on Schallberts Blog</title><link>https://blog.schallbert.de/categories/code/</link><description>Recent content in Code on Schallberts Blog</description><generator>Hugo</generator><language>de</language><lastBuildDate>Sun, 01 Nov 2026</lastBuildDate><atom:link href="https://blog.schallbert.de/categories/code/index.xml" rel="self" type="application/rss+xml"/><item><title>Docker-Images automatisch updaten</title><link>https://blog.schallbert.de/auto-upgrade-docker-images/</link><pubDate>Sun, 01 Nov 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/auto-upgrade-docker-images/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-11-01-docker-image-update-cover.avif"&#10; class="post-cover"&#10; alt="Image: A whale carries shipping containers on its back. One of the containers is open, and a crane hook updates the containing image with new content in huge bags."&#10; title="Docker-Images automatisch updaten" /&gt;&#10;&lt;h2 id="wozu-automatische-updates"&gt;Wozu automatische Updates?&lt;/h2&gt;&#10;&lt;p&gt;Software altert bzw. die Zeit streicht an ihr vorbei, sodass sie gewartet und auf Stand gehalten werden muss.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Software ist kein Vermögenswert, sondern eine Verbindlichkeit.&amp;rdquo; - &lt;a href="https://doctorow.medium.com/https-pluralistic-net-2026-01-06-1000x-liability-graceful-failure-modes-d69f384af9e4" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Cory Doctorow, Jan-2026&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;In meinem Blog habe ich schon mehrfach von Problemen wie kritischen Schwachstellen &lt;a href="https://blog.schallbert.de/gitea-critical-vulnerability-active-exploitation-rce/"&gt;in Gitea&lt;/a&gt; oder &lt;a href="https://blog.schallbert.de/php2shell-critical-vulnerability-rce/"&gt;in PHP&lt;/a&gt; berichtet, &lt;a href="https://blog.schallbert.de/gitea-out-of-memory/"&gt;Systemabstürze&lt;/a&gt; beseitigt und &lt;a href="https://blog.schallbert.de/repo-lookout-fix-deploy/"&gt;unabsichtlich veröffentlichten Seiten&lt;/a&gt; wieder eingefangen, deren Ursachen allesamt durch Updates oder Rekonfiguration behoben werden konnten.&lt;/p&gt;&#10;&lt;p&gt;Das Betriebssystem meines Servers und die wichtigsten darauf laufenden Programme erhalten bereits automatische Updates wie in meinem &lt;a href="https://blog.schallbert.de/server-auto-upgrade/"&gt;server-auto-upgrade Artikel&lt;/a&gt; beschrieben.&lt;/p&gt;&#10;&lt;p&gt;Trotzdem bin ich mit dem Thema noch nicht ganz fertig.&lt;/p&gt;&#10;&lt;h2 id="docker-image-update-mit-watchtower"&gt;Docker image update mit Watchtower&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-11-01-watchtower-decomissioning.avif" alt="Image: Decomissioning watchtower. A construction crane disassembles a watchtower, its lighting tip on the hook."&gt;&lt;/figure&gt;&#10;Um meine Docker-Images kümmerte sich bis Dato das Programm &lt;a href="https://github.com/containrrr/watchtower" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Watchtower&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Es gibt jedoch mehrere Gründe gegen seinen Einsatz:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Es wird nicht mehr aktiv gewartet und befindet sich im Archivzustand. Allerdings gibt es &lt;a href="https://github.com/nicholas-fedor/watchtower" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Forks, die weitergeführt werden&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/li&gt;&#10;&lt;li&gt;Um zu funktionieren, benötigt es Zugriff auf den Docker Daemon per &lt;code&gt;docker.sock&lt;/code&gt;. Dies ist &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;gleichbedeutend mit Root-Zugriff&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; des Containers auf mein System. Das will ich in Zukunft unbedingt vermeiden.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;1:1-Ersatz für Watchtower benötigt? &lt;a href="https://linuxhandbook.com/blog/watchtower-like-docker-tools/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Linuxhandbook hat hier ein paar Lösungen aufgelistet&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Allen gemein ist allerdings, dass sie entweder nur über vorhandene Updates informieren und sie nicht ausrollen, oder dass sie selbst ebenfalls den &lt;code&gt;docker.sock&lt;/code&gt; als Volume benötigen.&lt;/p&gt;&#10;&lt;h2 id="lösung-per-skript"&gt;Lösung per Skript&lt;/h2&gt;&#10;&lt;p&gt;Auf der Suche nach einem Weg ohne das Sicherheitsrisiko &lt;code&gt;docker.sock&lt;/code&gt; komme ich schnell zur Idee, täglich ein Skript per &lt;code&gt;cron&lt;/code&gt; ausführen zu lassen. Dies geschieht mit &lt;code&gt;root&lt;/code&gt;-Rechten und kann daher direkt Docker-Befehle ausführen.&lt;/p&gt;&#10;&lt;h3 id="kompliziert-docker-pull-abgleich-mit-vorhandenen-images-bei-bedarf-einspielen"&gt;Kompliziert: &lt;code&gt;docker pull&lt;/code&gt;, Abgleich mit vorhandenen Images, bei Bedarf einspielen&lt;/h3&gt;&#10;&lt;p&gt;Mein erstes Skript hat &lt;code&gt;164loc&lt;/code&gt; (Zeilen) und tut folgendes:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Prüfen, ob benötigte Werkzeuge &lt;code&gt;curl, docker, docker compose, docker-comppose.yml&lt;/code&gt; installiert sind und funktionieren&lt;/li&gt;&#10;&lt;li&gt;Die aktuellen &lt;a href="https://docs.docker.com/dhi/explore/security-concepts/digests/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;image digests&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; der in der &lt;code&gt;docker-compose.yml&lt;/code&gt; definierten Services herausfinden&lt;/li&gt;&#10;&lt;li&gt;Die Registry für jeden Service per &lt;code&gt;docker compose pull&lt;/code&gt; aktualisieren&lt;/li&gt;&#10;&lt;li&gt;Per &lt;code&gt;docker image inspect&lt;/code&gt; die &amp;ldquo;neuen&amp;rdquo; mit den &amp;ldquo;alten&amp;rdquo; image digests vergleichen&lt;/li&gt;&#10;&lt;li&gt;Die neuen Images per &lt;code&gt;docker compose up&lt;/code&gt; starten, Logs wegspeichern&lt;/li&gt;&#10;&lt;li&gt;Bericht per &lt;a href="https://ntfy.sh/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ntfy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; an mich, falls Updates erfolgt sind.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Doch dieser Weg ist viel zu kompliziert gedacht. Das von mir verwendete Werkzeug &lt;code&gt;docker compose&lt;/code&gt; bringt die benötigte Option direkt mit: &lt;a href="https://devops-daily.com/posts/docker-compose-always-recreate-containers" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;docker compose up --pull always&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="einfach-alles-in-einem-kommando-per-option-mitgeben"&gt;Einfach: Alles in einem Kommando per Option mitgeben&lt;/h3&gt;&#10;&lt;p&gt;Der Kern meines neuen Skriptes (&lt;code&gt;61loc&lt;/code&gt;) sieht wie folgt aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;compose&lt;span style="color:#f92672"&gt;=(&lt;/span&gt;docker compose --progress plain -f &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$COMPOSE_FILE&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;before&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;compose[@]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; ps --all --quiet | sort&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; output&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;compose[@]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; up -d --pull always --quiet-pull --wait &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --wait-timeout &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$WAIT_TIMEOUT&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; status&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; status&lt;span style="color:#f92672"&gt;=&lt;/span&gt;$?&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;printf &lt;span style="color:#e6db74"&gt;&amp;#39;[%s] docker compose up (exit %s)\n%s\n\n&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;date -Is&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$status&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$output&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$CHANGELOG&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;((&lt;/span&gt; status !&lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#f92672"&gt;))&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; report_failure &lt;span style="color:#e6db74"&gt;&amp;#34;WARNING: Docker update failed&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;warning&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$output&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; exit &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$status&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;after&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;compose[@]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; ps --all --quiet | sort&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[[&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$before&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; !&lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$after&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;]]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; notify &lt;span style="color:#e6db74"&gt;&amp;#34;INFO: Docker containers updated&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;white_check_mark&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$output&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Im Skript erhebe ich einen Status vor dem Update, indem alle in der Compose-Datei spezifizierten Services aufliste und in einer Variable zwischenparke. Anschließend führe ich wie üblich &lt;code&gt;docker compose up -d&lt;/code&gt; durch, allerdings nicht ohne die Flags &lt;code&gt;--pull always&lt;/code&gt; sowie &lt;code&gt;--wait wait-timeout &amp;lt;time[s]&amp;gt;&lt;/code&gt; zu setzen.&lt;/p&gt;&#10;&lt;p&gt;Mit der Pull-Option prüft Docker die Registry auf neue Images, welche gegebenenfalls heruntergeladen und gestartet werden. Der Timeout gibt dem Befehl eine bestimmte Zeit zur Ausführung bevor es mit einer Fehlermeldung abbricht, damit sich das Skript nicht aufhängen kann.&lt;/p&gt;&#10;&lt;p&gt;Was folgt ist nur noch Dokumentation und Monitoring: Die Ausgabe des Befehls wird in das Changelog geschrieben, im Falle eines Fehlschlags bekomme ich eine Information per &lt;a href="https://blog.schallbert.de/server-deploy-logging/#ntfy"&gt;Notify wie in meinem Server-Logging Post&lt;/a&gt; beschrieben. Hier wird der &lt;code&gt;before&lt;/code&gt;-Status wichtig: Ich eine Benachrichtigung nur bei Fehler oder Update.&lt;/p&gt;&#10;&lt;p&gt;Anschließend muss ich das Skript nur noch ausführbar machen &lt;code&gt;chmod + x docker-image-update.sh&lt;/code&gt; und an &lt;code&gt;root&lt;/code&gt; übergeben: &lt;code&gt;chown root root docker-image-update.sh&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h2 id="eintrag-für-cron"&gt;Eintrag für cron&lt;/h2&gt;&#10;&lt;p&gt;Nach einem manuellen Test des Skripts &lt;code&gt;./docker-image-update.sh&lt;/code&gt;, Prüfung seiner Ausgabe, des Changelogs und den neuen Containerversionen, lasse ich das Skript täglich durch den Cron-Daemon aufrufen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine: crontab -e&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; * * * /path/to/docker-image-update.sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="welche-version-wurde-installiert"&gt;Welche Version wurde installiert?&lt;/h2&gt;&#10;&lt;p&gt;Statt mit Versionsbezeichnungen der Services arbeitet Docker mit Image digests. Dieser &lt;code&gt;sha256-hash&lt;/code&gt; ist für jedes Image einzigartig, aber nicht besonders leserfreundlich:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sha256:94a00394bc5a8ef503fb59db0a7d0ae9e1110866e8aee8ba40cd864cea69ea1a &lt;span style="color:#75715e"&gt;# example Image digest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-11-01-ntfy-report-docker-image-update.avif" alt="Image: Screenshot of my phone&amp;#39;s Notify app message entry, showing an image update."&gt;&lt;/figure&gt;&#10;Doch Service-Versionsbezeichnungen wie &lt;code&gt;Gitea v28.2.1&lt;/code&gt; werden in den Image-Metadaten nicht standardmäßig unterstützt. Daher habe ich erstmal noch keine Lösung für die Ausgabe einer Version. Die Digests lasse ich in der Ausgabe weg.&lt;/p&gt;&#10;&lt;h2 id="meldung-bei-update"&gt;Meldung bei Update&lt;/h2&gt;&#10;&lt;p&gt;Die Meldung ist verbesserungsfähig: Ich benötige eigentlich nur die Namen der Container Updates und die Bestätigung, dass alle Images danach wieder &amp;ldquo;healthy&amp;rdquo; sind. Zusätzlich erhalte ich aber die Zustände der Container, aus denen ich die benötigten Informationen erst extrahieren muss. Der Mehrwert einer Filterung erscheint mir aber derzeit zu gering, als dass ich meine Zeit in die Erweiterung des Skriptes stecken würde. Schließlich zeigt mir &lt;code&gt;Recreate&lt;/code&gt; bereits an, welcher Container auf Stand gebracht wurde.&lt;/p&gt;&#10;&lt;p&gt;Ich bin zufrieden. Ich konnte vom Host aus mit wenigen Zeilen Code, aus Bordmitteln, und nur mit &lt;code&gt;cron&lt;/code&gt; als zusätzlicher Abhängigkeit mein Updateproblem für die per &lt;em&gt;Docker Compose&lt;/em&gt; verwalteten Container lösen.&lt;/p&gt;&#10;</description></item><item><title>PHP2shell: Kritische Sicherheitslücken</title><link>https://blog.schallbert.de/php2shell-critical-vulnerability-rce/</link><pubDate>Wed, 21 Oct 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/php2shell-critical-vulnerability-rce/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-21-php2shell-cover.avif"&#10; class="post-cover"&#10; alt="Image: The logo for php2shell. It is blue, block-like pixel text on black background."&#10; title="PHP2shell: Kritische Sicherheitslücken" /&gt;&#10;&lt;h2 id="schritt-1-erkennen-wir-haben-ein-problem"&gt;Schritt 1 Erkennen: Wir haben ein Problem.&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt;: PHP hat ganz aktuell &lt;a href="https://www.php.net/archive/2026.php#2026-10-20-1" target="_blank" rel="noopener noreferrer" class="external-link"&gt;zwei kritische Schwachstellen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die Remote Code Execution ermöglichen. Ihre IDs: &lt;a href="https://cert.europa.eu/publications/security-advisories/2026-018/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;CVE-2026-109618 und CVE-2026-109619&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Da ihre Ursachen nicht im Frontend sondern in der Engine stecken, hilft nur ein Update auf &lt;a href="https://watchtowr.com/intelligence/php-cve-2026-109618-faq/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PHP 8.5.12, 8.4.27, 8.3.36, oder 8.2.35&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-21-phps-php2shell-notice.avif" alt="Image: Screenshot of the PHP News Archive webpage on 21-Oct-2026 about a critical vulnerability allowing Remote Code Execution if exploited even before any application code runs."&gt;&lt;/figure&gt;&#10;&lt;p&gt;&amp;ldquo;&lt;a href="https://www.php.net/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PHP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; nutze ich nicht auf &lt;a href="https://blog.schallbert.de/projects/migrating-jekyll-to-hugo/"&gt;meiner Website&lt;/a&gt;&amp;rdquo;, denke ich, als ich heute Vormittag die folgende Nachricht erhalte:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Hi, kurzer Hinweis: Gegebenenfalls [&amp;hellip;] alles was php nutzt, erstmal abklemmen, [&amp;hellip;] Patch kommt später heute.&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Oder bin ich doch betroffen&amp;hellip;? Immerhin nutze ich eine managed Nextcloud-Instanz, und &lt;a href="https://docs.nextcloud.com/server/stable/developer_manual/getting_started/glossary.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Nextcloud ist doch u.A. in &lt;em&gt;PHP&lt;/em&gt; geschrieben&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;hellip;? Während ich diese Zeilen schreibe, &lt;a href="https://github.com/nextcloud/all-in-one/releases/tag/v14.4.1" target="_blank" rel="noopener noreferrer" class="external-link"&gt;wird ein Fix für Nextcloud verfügbar&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Alles muss also sehr schnell gehen. Heute werden unter Garantie viele Überstunden gemacht.&lt;/p&gt;&#10;&lt;h3 id="status-21102026-1030-mesz"&gt;Status 21.10.2026, 10:30 MESZ&lt;/h3&gt;&#10;&lt;p&gt;Spulen wir zehn Stunden zurück. Es gibt noch keinen Fix. Die Information über die CVEs wurde geleakt und ist jetzt selbst bereits ein paar Stunden alt. Laut &lt;a href="https://en.wikipedia.org/wiki/PHP2Shell" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Wikipedia nutzen &lt;code&gt;69%&lt;/code&gt; aller Websites PHP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Das Ausnutzen einer so hochkritischen Lücke (&lt;a href="https://blog.schallbert.de/gitea-critical-vulnerability-active-exploitation-rce/#cvss-scoring"&gt;Score&lt;/a&gt; &lt;code&gt;9.2&lt;/code&gt;) bei einer derart weitverbreiteten Technologie hat erwartbar massive Auswirkungen. Entsprechend nervös ist &amp;ldquo;das Internet&amp;rdquo;.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;[&amp;hellip;] organisations should consider isolating internet-facing applications from public networks until a fix is applied and verified. &lt;a href="https://cert.europa.eu/publications/security-advisories/2026-018/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;CERT-EU&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, pulled 21.10.2026, 10:40&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Interessant, die Empfehlung ist also ernsthaft &amp;ldquo;erstmal vom Netz nehmen&amp;rdquo;.&lt;/p&gt;&#10;&lt;h2 id="schritt-2-sofortmaßnahmen-nextcloud-vom-netz-trennen"&gt;Schritt 2 Sofortmaßnahmen: Nextcloud vom Netz trennen&lt;/h2&gt;&#10;&lt;p&gt;Um einem möglichen Angriff zuvor zu kommen versuche ich folglich, meine Instanz vom Netz zu nehmen. Dafür logge ich mich bei meinem Hoster ein, wechsle auf das Tab &amp;ldquo;Einstellungen&amp;rdquo;, und klicke auf den Reiter &lt;code&gt;ownCloud Console&lt;/code&gt;. Dies ist Nextclouds command-line interface.&lt;/p&gt;&#10;&lt;p&gt;Dort muss ich feststellen, dass kein Befehl zum Abschalten oder de-publishing zur Verfügung steht. Also wähle ich den Nächstbesten:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;occ maintenance:mode --on&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich versetze meine Instanz also &lt;a href="https://docs.nextcloud.com/server/stable/admin_manual/maintenance/backup.html#maintenance-mode" target="_blank" rel="noopener noreferrer" class="external-link"&gt;in den Wartungsmodus&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Dies verhindert neue Logins und friert die Datenbank ein. Anschließend lese ich mich ein wenig in die CVEs ein um herauszufinden, ob mein Schritt genügt.&lt;/p&gt;&#10;&lt;p&gt;Tut er wohl nicht.&lt;/p&gt;&#10;&lt;p&gt;In verschiedenen Foren und auf &lt;a href="https://thehackernews.com/2026/10/php2shell-two-unpatched-critical-php.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;HackerNews&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erfahre ich, dass eine nicht authentisierte Anfrage bereits den Angriff auslösen kann, bevor überhaupt PHP-Code auf der Serverseite ausgeführt wird.&lt;/p&gt;&#10;&lt;p&gt;Es ist also egal, in welchen Zustand ich meine Instanz versetze - solange sie am Netz ist, bleibt sie angreifbar.&lt;/p&gt;&#10;&lt;h2 id="schritt-3-vorläufige-eindämmung-nextcloud-instanz-deaktivieren-lassen"&gt;Schritt 3 Vorläufige Eindämmung: Nextcloud-Instanz deaktivieren lassen&lt;/h2&gt;&#10;&lt;p&gt;Ich finde auch mit Hilfe diverser Online-Suchanfragen keinen Weg, meine mangaged-Instanz abzuschalten. Ich überlege kurz, die Namensauflösung und das DNS-Routing zu killen. Mir wird aber schnell klar: Bis sich das durchpropagiert, ist der Fix längst draußen.&lt;/p&gt;&#10;&lt;h3 id="status-21102026-1143-mesz"&gt;Status 21.10.2026 11:43 MESZ&lt;/h3&gt;&#10;&lt;p&gt;Ich schreibe also ein Support-Ticket an meinen Hoster mit der Bitte um Hilfe beim Herunternehmen meiner Instanz.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-21-hoster-response-takedown.avif" alt="Image: Screenshot of an E-mail from my hoster, stating that my Nextcloud instance has been deactivated until a patch is available and in place."&gt;&lt;/figure&gt;&#10;&lt;h3 id="status-21102026-1201-mesz"&gt;Status 21.10.2026 12:01 MESZ&lt;/h3&gt;&#10;&lt;p&gt;Ich erhalte eine Mail vom Hoster!&lt;/p&gt;&#10;&lt;p&gt;Tatsache, ich kann die Instanz nicht mehr erreichen und erhalte eine &lt;code&gt;ERR_CERT_COMMON_NAME_INVALID&lt;/code&gt;-Meldung. Klicke ich diese weg, erscheint &lt;strong&gt;REQUESTED PAGE LOCKED&lt;/strong&gt;. Gute Arbeit. Die Reaktionszeit meines Anbieters inlusive Rückmeldung an den Kunden betrug &lt;code&gt;18min&lt;/code&gt;.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-21-instance-locked.avif" alt="Image: A flatscreen showing a &amp;#39;no entry&amp;#39; symbol, connected to multiple small boxes that look like mini servers. Image caption says &amp;#39;Requested page locked&amp;#39;."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Nun wechsle ich den Modus vom Akteur zum Beobachter. Ich sehe erste Exploit-Versuche (oder zumindest Beteuerungen davon) im Web und muss an eine Folie eines Vortrages denken (Zeitstempel &lt;code&gt;11:24&lt;/code&gt;), den ich vor kurzem gesehen habe: &lt;a href="https://kernel-recipes.org/en/2026/security-in-the-llm-age/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Security in the LLM age&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Dort zeigt ein Linux-Kernel Sachverständiger, dass wir in 2026 im Schnitt sieben Tage &lt;em&gt;vor&lt;/em&gt; einem Patch in betroffener Software bereits Exploits in freier Wildbahn sehen.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;discover --&amp;gt; disclose --&amp;gt; patch --&amp;gt; deploy&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Diese traditionelle Ereigniskette funktioniert nur bei &amp;ldquo;langsamen&amp;rdquo; Angreifern gut. Wir benötigen etwas, um wieder vor die Lage zu kommen.&lt;/p&gt;&#10;&lt;h2 id="schritt-4-beobachten-der-situation-verfügbarkeit-des-patches-einspielen-erster-betreiber"&gt;Schritt 4 Beobachten der Situation: Verfügbarkeit des Patches, Einspielen erster Betreiber&lt;/h2&gt;&#10;&lt;p&gt;Laut &lt;a href="https://thehackernews.com/2026/10/php-issues-emergency-patches-for-two.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hackernews&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; wird der Patch um etwa &lt;strong&gt;16:00 MESZ&lt;/strong&gt; verfügbar. Ich besorge mir den RSS-Feed meines Hosters und warte auf Meldung.&lt;/p&gt;&#10;&lt;h3 id="status-21102026-1420-mesz"&gt;Status 21.10.2026 14:20 MESZ&lt;/h3&gt;&#10;&lt;p&gt;Auch das Bundesamt für Sicherheit in der Informationstechnik hat einen &lt;a href="https://www.bsi.bund.de/SharedDocs/Cybersicherheitswarnungen/DE/2026/2026-297981-1032.pdf?__blob=publicationFile&amp;amp;v=4" target="_blank" rel="noopener noreferrer" class="external-link"&gt;BSI-IT-Sicherheitshinweis&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; herausgegeben, wo mit der &amp;ldquo;Kritikalität 3/Orange&amp;rdquo; vor der Lücke gewarnt und folgendes angemerkt wird:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Der Patch für die Schwachstelle ist heute 16 Uhr deutscher Zeit (14:00 UTC) angekündigt und sollte unverzüglich zu diesem Zeitpunkt installiert werden.&amp;rdquo; - BSI, 21.10.2026&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-21-hoster-vulnerability-notice.avif" alt="Image: Screenshot of my hoster&amp;#39;s notification feed for the PHP vulnerabilities in question. There are multiple entries at different points in time, all dated 21-Oct-2026."&gt;&lt;/figure&gt;&#10;&lt;h3 id="status-21102026-1634-mesz"&gt;Status 21.10.2026 16:34 MESZ&lt;/h3&gt;&#10;&lt;p&gt;Mein Hoster &lt;a href="https://status.hetzner.com/de/incident/f4144d55-38c3-48d1-8664-7a3429355cd7" target="_blank" rel="noopener noreferrer" class="external-link"&gt;meldet, dass die Patches nun verfügbar seien&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und mit der Implementierung begonnen würde. Betroffen sind nicht nur die Nextcloud-Instanzen, sondern auch seine komplette Administrationsoberfläche sowie sein Webmailer.&lt;/p&gt;&#10;&lt;h3 id="status-21102026-1952-mesz"&gt;Status 21.10.2026 19:52 MESZ&lt;/h3&gt;&#10;&lt;p&gt;Mein Hoster meldet Vollzug für die von ihm betriebenen Webhosting-Angebote und Managed Servers.&lt;/p&gt;&#10;&lt;h3 id="status-21102026-2240-mesz"&gt;Status 21.10.2026 22:40 MESZ&lt;/h3&gt;&#10;&lt;p&gt;Die Hetzner-Anwendung &lt;em&gt;KonsoleH&lt;/em&gt;, über die ich meine Nextcloud-Instanz verwalte, ist down.&lt;/p&gt;&#10;&lt;h3 id="status-21102026-2300-mesz"&gt;Status 21.10.2026 23:00 MESZ&lt;/h3&gt;&#10;&lt;p&gt;&lt;em&gt;KonsoleH&lt;/em&gt; ist wieder live (und gepatcht).&lt;/p&gt;&#10;&lt;h2 id="schritt-5-erkenntnis-und-schlussfolgerung"&gt;Schritt 5 Erkenntnis und Schlussfolgerung&lt;/h2&gt;&#10;&lt;p&gt;Was lerne ich aus den Ereignissen dieses turbulenten Tages? Wie gehe ich in Zukunft mit Sicherheitslücken um? Und wie erkenne ich überhaupt, dass ein Problem vorliegt?&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Nicht nur der Service als Ganzes ist angreifbar sondern auch seine Komponenten. &amp;ndash;&amp;gt; Kenne alle Komponenten und achte auf Schwachstellen und Patches!&lt;/li&gt;&#10;&lt;li&gt;Die Gegenspieler sind (durch LLMs) unglaublich schnell geworden. &amp;ndash;&amp;gt; Rechne mit Exploit vor Patch-Verfügbarkeit!&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Eine Liste geeigneter Quellen für Verwundbarkeiten, Neuigkeiten und Patches stelle ich mir gerade zusammen. Sobald die Liste stabil ist, werde ich darüber berichten.&lt;/p&gt;&#10;&lt;h2 id="schritt-6-aktionen"&gt;Schritt 6 Aktionen&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Sensible Daten wenn möglich aus dem Netz nehmen. Gedankengang: Was wäre, wenn das weg kommt? Reicht Verfügbarkeit im LAN/Intranet?&lt;/li&gt;&#10;&lt;li&gt;Besseres Monitoring und Abuse detection sind nur konsequent: Früh erkennen, früh sperren, früh melden, früh abschalten, sofort patchen.&lt;/li&gt;&#10;&lt;li&gt;Standardbezugsquellen für Informationen zu verwendeter Software besorgen. CVE-List-Scanner bauen? HackerNews-Filter?&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="nachtrag-22102026-0930-mesz"&gt;Nachtrag: 22.10.2026 09:30 MESZ&lt;/h3&gt;&#10;&lt;p&gt;Der Hoster hat meine Nextcloud-Instanz gepatcht und wieder online genommen. Alle Systeme normal. Es gibt nun auch einen &lt;a href="https://de.wikipedia.org/wiki/PHP2Shell" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Wikipedia-Eintrag&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zum Thema, der sehr aktuell gehalten wird.&lt;/p&gt;&#10;&lt;h2 id="schritt-7-erste-reflektion"&gt;Schritt 7 Erste Reflektion&lt;/h2&gt;&#10;&lt;p&gt;Nun sind ein paar Tage ins Land gegangen, seitdem die beiden Sicherheitslücken öffentlichkeiswirksam bekanntgemacht und Patches bereitgestellt wurden. Werfen wir mal einen Blick in die Netzlandschaft.&lt;/p&gt;&#10;&lt;h3 id="status-24102026-1630-mesz"&gt;Status 24.10.2026 16:30 MESZ&lt;/h3&gt;&#10;&lt;p&gt;Es gibt inzwischen viele, viele Machbarkeitsnachweise (PoC, Proof of Concept) für und über die Ausnutzung der PHP-Sicherheitslücken, &lt;a href="https://github.com/search?q=php2shell&amp;amp;type=repositories" target="_blank" rel="noopener noreferrer" class="external-link"&gt;allem voran auf der Plattform Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Ich selbst bin leider auf dem Themenfeld nicht sachkundig und muss mich auf die Einschätzung anderer verlassen, was Qualität dieser PoCs angeht, wie die es um die Ausnutzbarkeit in der freien Wildbahn wirklich bestellt ist und was die Hintergründe dazu betrifft, dass ausgerechnet diese Lücke medial so starke Resonanz zu erfahren scheint.&lt;/p&gt;&#10;&lt;p&gt;Im Allgemeinen gilt selbstverständlich &lt;strong&gt;&amp;ldquo;Erst patchen, dann nachfragen&amp;rdquo;&lt;/strong&gt;. So habe ich es ja auch gemacht. Wenn ich noch einmal Felix von Leitner zitieren darf:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Eine der Lektionen aus 20 Jahren Arbeit im Security-Umfeld ist, dass wenn einer behauptet, es gibt einen Bug, und der andere behauptet, der sei aber nicht exploitbar, dass sich dann im Nachhinein so gut wie immer raustellt, dass der doch exploitbar war.&amp;rdquo; - Feliks von Leitner aka &lt;a href="https://codeberg.org/tomas-jakobs/fefe-blog-backup/src/branch/main/content/2014/04" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Fefe, 12.April 2014&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, abgerufen am 24.10.2026&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Zur Ausnutzbarkeit schreibt die sich selbst als Bedrohungsaufklärungsseite &amp;ldquo;Threat intelligence&amp;rdquo; bezeichnende &lt;a href="https://flare.io/learn/resources/blog/php2shell-exploits-underground-analysis" target="_blank" rel="noopener noreferrer" class="external-link"&gt;flare.io&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, dass die meisten PoCs für PHP2shell nur in der Laborumgebung funktionierten. Auf echte Ziele seien sie kaum anwendbar - endweder, weil Betriebsinterna über die angegriffene Systeme erforderlich seien, weil sie andere Schwachstellen wie die Abschaltung von Speicherrandomisierung voraussetzen, oder weil die PoCs selbst &amp;ldquo;AI-Slop&amp;rdquo; darstellten: schlecht oder kaum validierte, mieserabel geschriebene Software, die mehr verspreche als sie halte.&lt;/p&gt;&#10;&lt;p&gt;Ähnlich äußert sich &lt;a href="https://eliastaylor.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;eliastaylor&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;There isn&amp;rsquo;t an easy exploit for this PHP vulnerability.&amp;rdquo; - &lt;a href="https://eliastaylor.com/noexploit/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;https://eliastaylor.com/noexploit/&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, abgerufen am 24.10.2026&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Sein Argument ist knapp zehn Stunden nach Veröffentlichung von Angriffsvektor und Patch, dass&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;keiner der Exploit-PoCs funktioniert, nicht einmal bei einer Standardinstallation und -Konfiguration von Pre-Patch-PHP&lt;/li&gt;&#10;&lt;li&gt;die beiden CVEs weit weniger kritisch sind als andere Sicherheitslücken der Geschichte (&lt;a href="https://en.wikipedia.org/wiki/Heartbleed" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Heartbleed&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bei Open SSL wird exemplarisch genannt)&lt;/li&gt;&#10;&lt;li&gt;die Exploits alle nicht von echten Leuten handgeschrieben, sondern mit Hilfe von LLMs generiert seien&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Der Kern der Argumentation geht dahin, dass Leute ihre Denkarbeit an Sprachmodelle abgäben und selbst gar nicht mehr prüften, ob das System tut was es sagt; geschweige denn den vom System geschriebenen Code läsen, verstünden und verbesserten. Bösartige Botnets würden noch immer von Menschen handgeschrieben, und das brauche seine Zeit, wenn die Lücken nicht offensichtlich und trivial auszunutzen seien.&lt;/p&gt;&#10;&lt;p&gt;Als kleine Übung für eine eigene Einschätzung der Lage empfehle ich die Lektüre der &amp;ldquo;Issue&amp;rdquo;-Diskussionen, die bei vielen PHP2Shell-PoCs auf Github öffentlich bereitstehen.&lt;/p&gt;&#10;&lt;p&gt;Das Große Medienecho hat aber auch etwas Gutes: &lt;a href="https://phpvulnerabilities.app/affected-products/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Die Welt patcht anscheinend schnell&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Ich habe mir die Resonanz auf diversen Foren, deutschen und internationalen, überflogen: Überall treiben Menschen ihre Hoster mit Patches vor sich her, diskutieren miteinander die Auswirkungen und welche betroffenen Anbieter bereits Updates herausgebacht haben.&lt;/p&gt;&#10;&lt;p&gt;Bleibt die Frage offen, wem das Ganze nützt. Ich habe schon ein paar Vermutungen. Für Schlussfolgerungen und meine Urteilsbildung ist es allderings noch zu früh.&lt;/p&gt;&#10;&lt;h3 id="status-27102026-1700-mesz"&gt;Status 27.10.2026 17:00 MESZ&lt;/h3&gt;&#10;&lt;p&gt;Die Sicherheitslücke gilt &lt;a href="https://www.cisa.gov/news-events/alerts/2026/10/26/cisa-adds-two-known-exploited-vulnerabilities-catalog" target="_blank" rel="noopener noreferrer" class="external-link"&gt;offiziell bestätigt als ausgenutzt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="schritt-8-beobachtungen-analysieren-und-informationskanäle-justieren"&gt;Schritt 8 Beobachtungen analysieren und Informationskanäle justieren&lt;/h2&gt;&#10;&lt;p&gt;Ich lege den Fokus nun auf Berichte zur Ausnutzung der Sicherheitslücken. Die Menge an POCs ist für Einzelpersonen unüberschaubar geworden. Erste Privatpersonen und &lt;a href="https://blog.cloudflare.com/php-ctf/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Firmen wie Cloudflare&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; haben begonnen, ein &lt;a href="https://en.wikipedia.org/wiki/Capture_the_flag_%28cybersecurity%29-Wettbewerbe" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Capture The Flag&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; auszuloben: Sie stellen absichtlich angreifbare PHP-Instanzen online und fordern auf, sie zu knacken und dies zu beweisen.&lt;/p&gt;&#10;&lt;p&gt;Ein sehr kluger Schritt, um die Spreu vom Weizen zu trennen. Auf diese Weise müssen die Leute ihre POCs gegen Sparringspartner ins Feld führen, die ihren Stack nicht offenlegen. Als zusätzliche Schwierigkeit fordern einige Anbieter auf, dass vorher außerdem andere ihrer Standard-Abwehrmechanismen wie &lt;a href="https://en.wikipedia.org/wiki/Web_application_firewall" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Web Application Firewalls&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; überwunden werden müssen. Schafft es ein POC hindurch, kann er als als unter realistischen Bedingungen durchgeführte Schwachstellenausnutzung gelten.&lt;/p&gt;&#10;&lt;h3 id="den-poc-verstehen"&gt;Den POC verstehen&lt;/h3&gt;&#10;&lt;p&gt;Ich habe (dank an &lt;a href="https://eliastaylor.com/altmans-cave/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;eliastaylor&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) den POC gefunden, welcher die &amp;ldquo;StealMyBitcoin&amp;rdquo; &lt;a href="https://github.com/KillaBoi/StealMyBitcoin-PoC/blob/main/stealmybitcoin_poc.py" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PHP2Shell-CTF gelöst hat&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Interessant sind sowohl die Kommentare in der Python-Datei über den Mechanismus des Exploits als auch die Diskussion auf Github zu Modellvariante, Prompt und Konfiguration des verwendeten LLM.&lt;/p&gt;&#10;&lt;p&gt;Ich verstehe die Quellen und Kommentare rund um diesen POC so, dass er die Probleme der vorigen Versuche elegant löst:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;PHP beim Multipart-Upload effektiv in die Speicherklemme bringen: Anstatt tausende Anfragen in kürzester Zeit zu senden (und zu hoffen, dass er den &amp;ldquo;richtigen&amp;rdquo; Worker-Thread trifft), verwendet diese Attacke &amp;ldquo;deeply nested field names&amp;rdquo;&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;, welche die Payload gering, jedoch den Speicheraufwand mit nur einer Anfrage serverseitig explodieren lassen.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Dies geht gegen die Lücke &lt;code&gt;CVE-2026-109618&lt;/code&gt; &amp;ldquo;out-of-memory handling can interrupt PHP while internal request data is only partially initialized. Request cleanup may then treat the invalid data as valid&amp;rdquo;. Ziel des &lt;a href="https://github.com/KillaBoi/StealMyBitcoin-PoC/blob/f394a6065f01f0afd48b867404017de01cd9811b/stealmybitcoin_poc.py#L228" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;heap_consumer&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist es nun, die herumliegenden, durch den Speicherüberlauf abgelaufene Daten zu manipulieren.&lt;/p&gt;&#10;&lt;ol start="2"&gt;&#10;&lt;li&gt;ASLR, randomisiertes Ablegen im Speicher umgehen: Anstatt ASLR im Feld zu ignorieren (oder in der Laborumgebung schlicht abzuschalten) müssen Speicherüberlauf, Manipulation und erneute Anfrage zum Einlesen der manipulierten Daten zwingend in derselben Session und auf demselben, überlasteten Worker geschehen&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. Ansonsten ist der betroffene Speicherbereich kaum wiederzufinden und wird irgendwann durch das System, welches den gestoppten Worker entdeckt, zurückgesetzt. Daher muss der Exploit den Schadcode bereits platziert haben, wenn PHP den zugehörigen Destruktor aufruft und damit schließlich die RCE ausführt.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Dies geht dann gegen die Zweite CVE im Bunde: &lt;code&gt;CVE-2026-109619&lt;/code&gt; &amp;ldquo;request-owned internal state can survive after its backing memory has been freed. A later request handled by the same worker may use that stale state before normal initialization resets it.&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;Wäre jetzt cool hierzu mal eine PHP-Engine-Sachverständige zu hören :)&lt;/p&gt;&#10;&lt;p&gt;Damit steht auch der dunklen Seite in der Breite ein Exploit zur Verfügung, der bei &amp;ldquo;einfachen Zielen&amp;rdquo; ohne Härtung eine (automatisierte) Ausnutzung machbar erscheinen lässt.&lt;/p&gt;&#10;&lt;h3 id="härtung"&gt;Härtung&lt;/h3&gt;&#10;&lt;p&gt;Lösen kann das Problem nur der Patch. Folgende Härtungsmaßnahmen können gegen diesen speziellen Angriffsvektor zumindest den Widerstand erhöhen:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Das Kommando &lt;code&gt;PHP_SESSION_UPLOAD_PROGRESS&lt;/code&gt;, durch den der Angriff durchgeführt wird, gar nicht erst anbieten oder aber keine Anfragen in sehr kurzer Abfolge, in derselben Session, oder von derselben Maschine zulassen.&lt;/li&gt;&#10;&lt;li&gt;Die Anzahl Worker auf Maximum drehen, auch wenn dies auf die Leistung der Maschine geht. Dies macht das Wiedereinfangen und Ausnutzen desjenigen Workers schwieriger, welcher zuvor korrumpiert wurde.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="später-am-selben-tag"&gt;Später am selben Tag&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://thehackernews.com/2026/10/cisa-confirms-active-exploitation-of.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;The Hacker News&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bestätigt die Ausnutzung und zählt betroffene Firmen bzw. Großkonzerne und Lieferanten auf. Der Tenor: Patchen, patchen, patchen!&lt;/p&gt;&#10;&lt;h2 id="status-28102026-2130-mesz"&gt;Status 28.10.2026 21:30 MESZ&lt;/h2&gt;&#10;&lt;p&gt;Cloudflare hat inzwischen neue Firewall-Regeln für &lt;a href="https://developers.cloudflare.com/waf/change-log/changelog/#2026-10-28" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ihre WAF&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; veröffentlicht, die Angreifende schon vor Erreichen der Zielserver abfangen soll. &lt;a href="https://thehackernews.com/2026/10/php2shell-attack-roots-25-servers.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hackernews&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; berichtet über konkrete Vorfälle mit ersten Schäden. &lt;a href="https://thehackernews.com/2026/10/attackers-exploit-php2shell-flaws-in.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Threat intelligence Firmen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bestätigen dies.&lt;/p&gt;&#10;&lt;p&gt;Zum Abrunden des Status hier noch eine &lt;a href="https://yourmom2shell.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nette Sammlungsseite mit Informationen über x-2-shell-Schwachstellen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/KillaBoi/StealMyBitcoin-PoC/blob/f394a6065f01f0afd48b867404017de01cd9811b/stealmybitcoin_poc.py#L218" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Quelle: Exploit-Code&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;&lt;a href="https://infosecmonkey.com/php2shell-cve-2026-109618-109619-explained/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Quelle: infosecmonkey.com&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Passwortmanager mit Security Keys verwenden</title><link>https://blog.schallbert.de/protecting-keepassxc-with-hardware-security-keys/</link><pubDate>Sun, 18 Oct 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/protecting-keepassxc-with-hardware-security-keys/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-18-keepassxc-unlock-database-popup-cover.avif"&#10; class="post-cover"&#10; alt="Image: KeePassXC&amp;#39;s database unlock popup as presented on Linux and Windows machines. The option &amp;#39;use hardware key&amp;#39; is ticked."&#10; title="Passwortmanager mit Security Keys verwenden" /&gt;&#10;&lt;h2 id="der-passwortmanager"&gt;Der Passwortmanager&lt;/h2&gt;&#10;&lt;p&gt;Das Netz ist voll mit verschiedenen Lösungen zum Passwortmanagement. Das ist Nachvollziehbar, denn wir haben immer mehr Accounts und niemand merkt sich eine dreistellige Anzahl an Passwörtern freiwillig. Kommen wir also gleich zur Sache:&lt;/p&gt;&#10;&lt;p&gt;Ich habe mir aus dem ganzen Wust drei Passwortmanager nach meinen Kriterien (Sicher, komfortabel, quelloffen, selbstverwaltbar) angesehen und wollte hieraus eine Wahl treffen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://support.mozilla.org/en-US/kb/password-manager-remember-delete-edit-logins" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Firefox password manager&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (Online/Offline)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://vaultwarden.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Vaultwarden&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (Online)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://keepassxc.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;KeePassXC&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;/&lt;a href="https://www.keepassdx.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;KeePassDX&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (Offline)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="kein-firefox-password-manager"&gt;Kein Firefox Password manager&lt;/h3&gt;&#10;&lt;p&gt;Firefox schied aus, da ich zum Entsperren meinen &lt;a href="https://blog.schallbert.de/hardware-security-keys-yubikey/"&gt;Hardware Security Key&lt;/a&gt; verwenden und mir kein langes Hauptpasswort merken möchte. Für den Einsatz von Firefox als Passwortmanager sprachen hingegen die sehr einfache Verwendung, das automatische Ausfüllen von Passwortfeldern und die Möglichkeit der Synchronisation, wenn auch über eine fremde Infrastruktur.&lt;/p&gt;&#10;&lt;h3 id="kein-vaultwarden"&gt;Kein Vaultwarden&lt;/h3&gt;&#10;&lt;p&gt;Vaultwarden hatte ich lange im Visier. Es benötigt für den Betrieb im lokalen Netzwerk viele Vorbedingungen und Software drum herum, Stichworte &lt;a href="https://helgeklein.com/blog/automatic-https-certificates-for-services-on-internal-home-network-without-opening-firewall-port/#dns-challenge--caddy-to-the-rescue" target="_blank" rel="noopener noreferrer" class="external-link"&gt;HTTPS im LAN&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://letsencrypt.org/docs/challenge-types/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ACME-Challenge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Daher ist im LAN ein eigener kleiner Rechner vorzusehen und idealerweise verfügt man sogar über einen virtuellen privaten Server (VPS), der den HTTPS-Verkehr terminieren kann.&lt;/p&gt;&#10;&lt;p&gt;Dafür unterstützt Vaultwarden dann sogar die super bequeme Anmeldung per Passkey und kann die Passwortdatenbank zwischen allen Geräten im Netzwerk synchronisieren.&lt;/p&gt;&#10;&lt;p&gt;Doch am Ende scheute ich die aufwendige Einrichtung und die neuen Abhängigkeiten und Softwarepakete, die ich alle hätte zusätzlich warten müssen.&lt;/p&gt;&#10;&lt;h3 id="keepassxc"&gt;KeePassXC&lt;/h3&gt;&#10;&lt;p&gt;Ich habe mit verschiedenen Varianten von KeePass herumgespielt und mich am Ende für KeePassXC entschieden. Das Gesamtpaket fühlt sich einfach rund und modern an, ich kann die Software betriebssystemunabhängig nutzen und die benötigten Erweiterungen zur Verwendung mit HSKs sind auch gleich dabei.&lt;/p&gt;&#10;&lt;h2 id="einrichtung-von-keepassxc-mit-einem-hsk"&gt;Einrichtung von KeePassXC mit einem HSK&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-18-keepassxc-setup-database-hsk.avif" alt="Image: KeePassXC create new database screen. There is a main password field, a security key field to protect with a local/remote key file, and a Challenge-response field to work with Hardware Security Keys."&gt;&lt;/figure&gt;&#10;KeePass verwendet das &lt;a href="https://en.wikipedia.org/wiki/HMAC" target="_blank" rel="noopener noreferrer" class="external-link"&gt;keyed-hash message authentication code&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; oder HMAC-Verfahren zum Entsperren der Passwortdatenbank unter Verwendung von Hardware Security Keys. Es basiert auf einem gemeinsamen geheimen Schlüssel, der bei beiden Parteien vorliegen muss, aber nicht zwischen den Parteien ausgetauscht wird.&lt;/p&gt;&#10;&lt;p&gt;Üblicherweise erstellt man den geheimen Schlüssel auf dem Hardware Security Key und kopiert ihn dann in die Eingabemaske von KeePassXC - aber der Reihe nach, denn der Vorgang hängt ein wenig vom verwendeten HSK ab.&lt;/p&gt;&#10;&lt;h3 id="yubikey"&gt;Yubikey&lt;/h3&gt;&#10;&lt;p&gt;Yubico hat eine Anleitung eigens für die &lt;a href="https://support.yubico.com/s/article/Using-your-YubiKey-with-KeePass" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Verwendung mit KeePass&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; vorbereitet. Ein Yubikey hat zwei variabel verwendbare &lt;a href="https://docs.yubico.com/yesdk/users-manual/application-otp/slots.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Slots&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, welche verschiedene kryptografische Funktionen unterstützen. In der Funktionsauswahl ist &lt;code&gt;Challenge-response: program a challenge-response credential&lt;/code&gt; anzuwählen. Anschließend erzeugt der Yubikey einen &lt;code&gt;20-byte HEX code&lt;/code&gt;, welcher anschließend nach KeePassXC kopiert werden kann.&lt;/p&gt;&#10;&lt;p&gt;Wichtig dabei ist, dass der angezeigte Code später nicht mehr extrahiert werden kann. Soll die Datenbank also einen Zweitschlüssel bekommen (was anzuraten ist), so muss der Code notiert und anschließend ebenfalls im Zweitschlüssel hinterlegt werden.&lt;/p&gt;&#10;&lt;p&gt;Ich empfehle beim Yubikey, das Häkchen bei &lt;code&gt;requires touch&lt;/code&gt; wegzulassen. Ansonsten muss man bei jeder Verwendung eines Passwortes den Yubikey berühren. Viel nerviger noch ist, dass für das Hinzufügen oder Ändern eines Passworteintrages ebenfalls ein Berühren des Schlüssels notwendig wird.&lt;/p&gt;&#10;&lt;h3 id="nitrokey"&gt;Nitrokey&lt;/h3&gt;&#10;&lt;p&gt;Nitrokey sieht die Erzeugung des Geheimnisses als &lt;code&gt;20-byte base32 code&lt;/code&gt; vor. Er kann genau ein HMAC-Geheimnis aufbewahren.&#10;Auch Nitrokey hat eine &lt;a href="https://docs.nitrokey.com/software/nk-app2/keepassxc" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung zum Hinterlegen des HMAC-Secrets&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für die Verwendung mit KeePassXC online. Wie bei Yubikey auch basiert diese Anleitung auf der Verwendung der App.&lt;/p&gt;&#10;&lt;h2 id="kompatibilität-yubikey"&gt;Kompatibilität Yubikey&lt;/h2&gt;&#10;&lt;p&gt;Idealerweise würde ich nun den auf dem Yubikey erzeugten Schlüssel auf dem Nitrokey hinterlegen oder umgekehrt. So hätte ich einen Zweitschlüssel und könnte mit beiden auf die KeePassXC-Datenbank zugreifen.&lt;/p&gt;&#10;&lt;p&gt;Doch oh weh, der Nitrokey akzeptiert das Geheimnis des Yubikey nicht:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-18-nitrokey-challenge-response-does-not-accept-yubi-key.avif" alt="Image: The Nitrokey app, being confronted with a Yubikey HMAC secret, returns the error message &amp;#39;Secret is not in base32.&amp;#39; and cannot save the key"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Genau so verhält es sich, wenn ich ein mit dem Nitrokey erzeugtes Geheimnis auf dem Yubikey hinterlegen will:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-18-yubikey-challenge-response-does-not-accept-nitro-key.avif" alt="Image: The Yubikey app, being confronted with a Nitrokey HMAC secret, returns the error message &amp;#39;Invalid format, allowed characters: 0123456789abcdef&amp;#39; and cannot save the key"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Hierfür benötige ich Abhilfe. Ich erstelle also ganz pragmatisch eine zweite Passwortdatenbank. Beide sollen denselben Inhalt haben, verwenden aber verschiedene Schlüssel zum Öffnen. Später lerne ich, dass das gar nicht nötig ist; dennoch hier erstmal meine provisorischen Lösungen.&lt;/p&gt;&#10;&lt;h3 id="holzweg-a-einträge-aus-der-datenbank-klonen"&gt;Holzweg A: Einträge aus der Datenbank klonen&lt;/h3&gt;&#10;&lt;p&gt;Ich befülle die erste Datenbank mit Passwörtern, welche ich in Gruppen sortiere. Anschließend klone ich die Gruppe, öffne die zweite Datenbank und ziehe den Klon per drag-and-drop herüber.&lt;/p&gt;&#10;&lt;p&gt;Nachteile: Ich muss den Vorgang für jede Gruppe wiederholen, denn drag-und-drop für die &lt;code&gt;root&lt;/code&gt;-Gruppe ist nicht vorgesehen. Bei jeder Änderung in &lt;code&gt;Datenbank_yubikey&lt;/code&gt; muss ich &lt;code&gt;Datenbank_nitrokey&lt;/code&gt; ebenfalls anpassen.&lt;/p&gt;&#10;&lt;p&gt;Das ist sehr anstrengend, wenn die Datenbank sich noch im Aufbau befindet.&lt;/p&gt;&#10;&lt;h3 id="holzweg-b-die-ganze-datenbank-kopieren-und-hmac-updaten"&gt;Holzweg B: Die ganze Datenbank kopieren und HMAC updaten&lt;/h3&gt;&#10;&lt;p&gt;Hierfür speichert man die HMAC-Secrets der beiden Hardware Security Keys im Passwortmanager ab. Bei einer Änderung der Datenbank fertigt man eine Kopie der Datenbankdatei an, öffnet sie und kopiert sich das Secret des jeweils anderen HSK. Nun klickt man auf &lt;code&gt;Database-&amp;gt;Database Security-&amp;gt;Change Challenge-Response&lt;/code&gt; und gibt das Secret dort ein. Schon funktioniert die Datenbank mit dem anderen Schlüssel.&lt;/p&gt;&#10;&lt;p&gt;Nachteil: Manuelle Aktualisierung beider Datenbanken erforderlich. Jedes mal ist challenge-response neu einzurichten.&lt;/p&gt;&#10;&lt;h3 id="lösung-konvertieren-des-hmac-secrets"&gt;Lösung: Konvertieren des HMAC-Secrets&lt;/h3&gt;&#10;&lt;p&gt;Ich fragte mich, ob ich für den Nitrokey zur Speicherung des HMAC-Geheimnisses für meinen Nitrokey auch die Kommandozeile statt der App verwenden kann. Kommanodzeilenanwendungen wie &lt;code&gt;nitropy&lt;/code&gt; verfügen meist über mehr Optionen als ihr GUI-App-Pendant. Vielleicht kann ich damit das Kompatibilitätsproblem lösen. Hier der &lt;a href="https://github.com/Nitrokey/nitrokey-documentation/blob/main/source/components/software/nitropy/all-platforms/installation.rst" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Link zur Installation von nitropy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Auf der Suche nach einer geeigneten Dokumentation stoße ich auf &lt;a href="https://support.nitrokey.com/t/setup-otp-challenge-response-for-keepassxc/5497" target="_blank" rel="noopener noreferrer" class="external-link"&gt;diesen interessanten Post zum Thema challenge-response&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Dort wird mir klar, was softwareaffine Leute schon lange wissen. Zumindest, falls sie entweder mal das Thema Kryptografie gestreift haben oder nahe an der Hardware unterwegs sind:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Daten der Größe von &lt;code&gt;20 Byte&lt;/code&gt; lassen sich in &lt;code&gt;hex&lt;/code&gt; mit 40 Zeichen UND in &lt;code&gt;base32&lt;/code&gt; mit 32 Zeichen darstellen.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Folglich lässt sich ein HMAC-Geheimnis konvertieren und damit sowohl auf dem Yubikey als auch auf dem Nitrokey ablegen. Dafür verwende ich das kleine Werkzeug &lt;a href="https://linux.die.net/man/1/xxd" target="_blank" rel="noopener noreferrer" class="external-link"&gt;xxd&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und lasse mir aus dem Hexcode des Yubikey eine &lt;code&gt;base32&lt;/code&gt;-Phrase für meinen Nitrokey erzeugen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# open a new terminal&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# make sure the secrets are not saved to history (command for zsh)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;history -p &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# c0ffee = secret hex key&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# xxd convert input to binary (-r), output as plain hexdump (-p)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# interpret as base32 string&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo -n &lt;span style="color:#e6db74"&gt;&amp;#39;c0ffee&amp;#39;&lt;/span&gt; | xxd -r -p | base32&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;YD764&lt;span style="color:#f92672"&gt;===&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# close terminal&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Alternativ funktioniert auch der Rückweg:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# open a new terminal&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# make sure the secrets are not saved to history (command for zsh)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;history -p &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# YD764=== = secret base32 key&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo -n &lt;span style="color:#e6db74"&gt;&amp;#39;YD764===&amp;#39;&lt;/span&gt; | base32 -d | xxd -p&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;c0ffee&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# close terminal&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="keepassxc-passwörter-schön-geordnet-hinterlegen"&gt;KeePassXC: Passwörter schön geordnet hinterlegen&lt;/h2&gt;&#10;&lt;p&gt;Verzichtet man wie ich auf Dialoge wie &amp;ldquo;mit Google einloggen&amp;rdquo; bei Drittanbietern, so sammeln sich im Laufe des Lebens unter Umständen eine Menge Passwörter an. Zum vereinfachten Finden und verwenden der Passwörter sowohl mobil als auch auf dem Desktop bietet es sich an, sinnvolle Gruppen zu erstellen.&lt;/p&gt;&#10;&lt;h3 id="passwortfamilien"&gt;Passwortfamilien&lt;/h3&gt;&#10;&lt;p&gt;Nach ein paar Experimenten hat sich folgendes Schema für Titel bewährt, wenn ich mehrere verwandte Passwörter speichern möchte:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-py" data-lang="py"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;baseapp&lt;span style="color:#f92672"&gt;.&lt;/span&gt;attribute&lt;span style="color:#f92672"&gt;.&lt;/span&gt;specialization&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So würde aus den Zugängen für mein &lt;a href="https://blog.schallbert.de/gitea-critical-vulnerability-active-exploitation-rce/"&gt;gehärtetes Gitea-setup&lt;/a&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-py" data-lang="py"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea&lt;span style="color:#f92672"&gt;.&lt;/span&gt;login&lt;span style="color:#f92672"&gt;.&lt;/span&gt;schallbert&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea&lt;span style="color:#f92672"&gt;.&lt;/span&gt;login&lt;span style="color:#f92672"&gt;.&lt;/span&gt;testuser&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea&lt;span style="color:#f92672"&gt;.&lt;/span&gt;runner&lt;span style="color:#f92672"&gt;.&lt;/span&gt;registration&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea&lt;span style="color:#f92672"&gt;.&lt;/span&gt;recoverykey&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Auf diese Weise bleiben verwandte Passwörter schön beisammen.&lt;/p&gt;&#10;&lt;h3 id="gruppierung"&gt;Gruppierung&lt;/h3&gt;&#10;&lt;p&gt;Ich halte folgende Gruppen von Passwörtern für sinnvoll:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Group&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Description&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Devices&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Computer, phone, router, keys&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Finance&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Banks, credit cards, insurances, payment&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Identity&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Email, identity providers, single sign-in&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Shopping&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Online stores, streaming, subscriptions&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Games&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Platforms, launchers, servers&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Social&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Forums, Messengers, Social networks&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Backup&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Recovery keys, licenses, passphrases&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Dabei achte ich darauf, nicht zu viele Untergruppen zu erstellen. Gerade in der Mobilansicht ist man sonst schnell verwirrt.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-18-keepassxc-password-groups.avif" alt="Image: KeePassXC app with unlocked database. We see different groups of passwords and single entries, each arranged nicely with a logo/symbol image."&gt;&lt;/figure&gt;&#10;&lt;h3 id="icons-"&gt;Icons 🔑&lt;/h3&gt;&#10;&lt;p&gt;KeePassXC bietet die wunderbare Möglichkeit, Favicons der Zielwebsite herunterzuladen und als Symbol zu verwenden. Damit gelingt die Navigation im Passwortmanager noch einfacher.&lt;/p&gt;&#10;&lt;p&gt;Die Favicons sind allerdings alles Bitmaps. Möchte man hochqualitative Vektorgrafiken verwenden bietet sich an, unter &amp;ldquo;Choose Icon&amp;rdquo; den Pfad eines Icon Packs anzugeben. Ich habe mir dafür die &lt;a href="https://aegis-icons.github.io/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Aegis icons&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; heruntergeladen und bin damit sehr zufrieden.&lt;/p&gt;&#10;</description></item><item><title>Kritische Schwachstellen bei Gitea (und Forgejo)</title><link>https://blog.schallbert.de/gitea-critical-vulnerability-active-exploitation-rce/</link><pubDate>Sun, 04 Oct 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/gitea-critical-vulnerability-active-exploitation-rce/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-04-gitea-lower-than-V27-vulnerable-cve.avif"&#10; class="post-cover"&#10; alt="Image: The logo of Gitea, a cup of tea. It is boiling hot, and steam is emanating from the cup&amp;#39;s contents. In one of the steam clouds, there is a Jolly Roger. The image caption says &amp;#39;Gitea "&#10; title="Kritische Schwachstellen bei Gitea (und Forgejo)" /&gt;&#10;&lt;aside class="update-box update-box--error" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⛔&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: Forgejo v16.0.4 und abwärts ebenfalls betroffen&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-10-02T00:00:00Z"&gt;&#10; 02. Okt. 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Die &lt;a href="https://forgejo.org/releases/16.x/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Release notes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; lesend, betrachte ich die Software-Forge-Lösung &lt;a href="https://forgejo.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Forgejo&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (ein Fork von Gitea) als ebenfalls von den unten beschriebenen CVEs betroffen. Erst mit v16.0.5 vom 17.09.2026 aufwärts sind sämtliche in diesem Artikel diskutierten CVEs behoben. Kommt im Artikel also das Wort &amp;lsquo;Gitea&amp;rsquo; vor, ist Forgejo ausdrücklich mitgemeint.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="gewahrwerdung"&gt;Gewahrwerdung&lt;/h2&gt;&#10;&lt;p&gt;Wie so oft beginnt auch diese Geschichte mit einem Netzwerk der menschlichen Kommunikation. Dieser &lt;a href="https://blog.schallbert.de/gitea-critical-vulnerability-active-exploitation-rce/#credits"&gt;unersetzliche Austausch&lt;/a&gt; hat schon viel zur Sicherheit der Software beigetragen, mit der ich diesen Blog betreibe. Nun kann ich eine weitere Sicherheitslücke schließen. Aber der Reihe nach.&lt;/p&gt;&#10;&lt;h3 id="gitea-als-angriffsziel"&gt;Gitea als Angriffsziel&lt;/h3&gt;&#10;&lt;p&gt;Ich hörte, dass die selbst gehostete Versionsverwaltungslösung &lt;a href="https://en.wikipedia.org/wiki/Gitea" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; eines meiner Bekannten Ziel einer Attacke geworden ist. Hierbei wurde eine &lt;a href="https://www.crowdstrike.com/en-us/cybersecurity-101/cyberattacks/remote-code-execution/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;RCE&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (Remote Code Execution) möglich und tatsächlich auch ausgeführt.&lt;/p&gt;&#10;&lt;h3 id="warum-ist-gitea-ein-attraktives-ziel"&gt;Warum ist Gitea ein attraktives Ziel?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Diebstahl von (Firmen)geheimnissen: Bei Zugriff auf Repositories ist vertraulicher Quellcode gefährdet, offengelegt zu werden.&lt;/li&gt;&#10;&lt;li&gt;Abfluss von Zugangsdaten: Wird ein größeres Produkt mit Schnittstellen in Gitea verwaltet, sind &lt;a href="https://en.wikipedia.org/wiki/API_key" target="_blank" rel="noopener noreferrer" class="external-link"&gt;API keys&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, Konten und Passwörter in Gefahr, falls sie unverschlüsselt vorliegen. Abhilfe schaffen Tools wie &lt;a href="https://dev.to/vnjogani/a-guide-to-git-secret-49g3" target="_blank" rel="noopener noreferrer" class="external-link"&gt;git-secret&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;; sie werden aber durch Einführen zusätzlicher Komplexität noch nicht flächendeckend eingesetzt.&lt;/li&gt;&#10;&lt;li&gt;Klassische Rechteeskalation: Unter Umständen kann man durch Ausbruch aus der Gitea-Software die ganze Maschine übernehmen.&lt;/li&gt;&#10;&lt;li&gt;Nutzen des &lt;a href="https://gitea.com/gitea/runner" target="_blank" rel="noopener noreferrer" class="external-link"&gt;runner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;: Schafft man es den Runner zu kapern, kann man auch ohne erweiterte Rechte Ressourcen der Hostmaschine in Beschlag nehmen und für eigene Zwecke missbrauchen (Stichwort &lt;a href="https://dev.to/golu12/crypto-mining-is-killing-all-free-cicd-platforms-28ip" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Cryptominers in CI/CD&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;).&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="was-ist-eine-rce"&gt;Was ist eine RCE?&lt;/h3&gt;&#10;&lt;p&gt;Eine Remote Code Execution ist ein erfolgreicher Angriff, bei dem nicht autorisierter Code ohne Zutun von Besitzer oder Administration der befallenen Ressource aus der Ferne über das Netzwerk eingespielt und ausgeführt werden kann.&lt;/p&gt;&#10;&lt;h2 id="das-gitea-ökosystem"&gt;Das Gitea-Ökosystem&lt;/h2&gt;&#10;&lt;p&gt;Als Versionsverwaltung und &lt;a href="https://en.wikipedia.org/wiki/Forge_%28software%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;forge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gehört zum Basisumfang von Gitea die Möglichkeit, webbasiert und kollaborativ Software zu entwickeln und zu bauen. Daher ist vorgesehen, dass sich Nutzer registrieren und unter Umständen sogar &lt;a href="https://github.com/features/actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Aktionen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ausführen können, damit Software automatisiert gebaut und/oder ausgeliefert werden kann.&lt;/p&gt;&#10;&lt;p&gt;Genau dieses automatische Nachladen und Ausführen von Code kann böse Folgen haben, wenn Sicherheitslücken in der Verwaltungssoftware selbst bestehen.&lt;/p&gt;&#10;&lt;h3 id="verwundbarkeit-durch-offene-registrierung"&gt;Verwundbarkeit durch offene Registrierung&lt;/h3&gt;&#10;&lt;p&gt;Lässt man als Betreiberin die Registrierung neuer Accounts (&amp;ldquo;self-registration&amp;rdquo;) in Giteas Konfiguration auf dem Standardwert &lt;code&gt;DISABLE_REGISTRATION = false&lt;/code&gt;, so können sich Nutzerinnen selbst einschreiben, eigene Repository anlegen, issues aufmachen und alles tun, was ohne Adminrechte möglich ist.&lt;/p&gt;&#10;&lt;p&gt;Hat man zusätzlich noch den Standardwert &lt;code&gt;REQUIRE_SIGNIN_VIEW = false&lt;/code&gt; gesetzt, können auch unauthentisierte Besucher öffentliche Repositories sehen, die Seiten von Gitea browsen und vor allem die öffentliche &lt;a href="https://docs.gitea.com/api/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;API des Gitea Endpoints&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ansprechen.&lt;/p&gt;&#10;&lt;h3 id="verwundbarkeit-durch-standardkonfiguration-für-reverse-proxies"&gt;Verwundbarkeit durch Standardkonfiguration für Reverse Proxies&lt;/h3&gt;&#10;&lt;p&gt;In der Standardkonfiguration des Gitea Docker image vertraut Gitea &lt;code&gt;&amp;lt;V1.26.3&lt;/code&gt; allen Proxies.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/app.ini&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[security]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REVERSE_PROXY_LIMIT&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REVERSE_PROXY_TRUSTED_PROXIES&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;* # This is a vulnerability!&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="die-sicherheitslücken"&gt;Die Sicherheitslücken&lt;/h2&gt;&#10;&lt;p&gt;Womit haben wir es hier zu tun? Mit mehreren kritischen Sicherheitslücken des Ausmaßes &lt;code&gt;9.8/10&lt;/code&gt; &lt;a href="https://www.first.org/cvss/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;CVSS score&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, von denen es bekannte Fälle der Ausnutzung gibt.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-04-cvss-calculator.avif" alt="Image: Common vulnerability scoring system calculator. The selectors are switched in a way that reproduces a critical vulnerability over a network vector without user interaction as discussed in this post."&gt;&lt;/figure&gt;&#10;&lt;h3 id="cvss-scoring"&gt;Exkurs: Die CVSS-Bewertung&lt;/h3&gt;&#10;&lt;p&gt;Zur Einordnung der Bewertung von &amp;ldquo;kritisch&amp;rdquo;: Die CVSS-Kriterien sind ähnlich aufgebaut wie in der klassischen Gefahren- und &lt;a href="https://de.wikipedia.org/wiki/Risikoanalyse" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Risikoanalyse&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, einem Zweig der Ingenieurswissenschaften. Anders als dort liegt der Fokus allerdings nicht in der Analyse möglicher Fehlerfälle, Schweregrad, Eintritts- und Ausfallwahrscheinlichkeit oder Detektierbarkeit, sondern auf dem Schwierigkeitsgrad der Ausnutzung und möglicher Auswirkungen.&lt;/p&gt;&#10;&lt;h3 id="bedeutung-der-cvss-einzelwerte"&gt;Bedeutung der CVSS-Einzelwerte&lt;/h3&gt;&#10;&lt;p&gt;Je höher der CVSS-Wert, desto einfacher ist die Sicherheitslücke auszunutzen und desto gravierender sind mögliche Folgen hinsichtlich der Datensicherheitskriterien (&lt;a href="https://en.wikipedia.org/wiki/CIA_triad" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Confidentiality, Integrity, Availability&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;).&lt;/p&gt;&#10;&lt;p&gt;Das Bild oben zeigt eine Konstellation, in der eine mögliche Attacke aus der Ferne (Netzwerk) mit geringem Aufwand (Complexity, meint Systemhärtung) ohne Randbedingungen (Requirement, z.B. das Anfahren einer vulnerablen Stelle im Code, Vorhandensein eines bestimmten Zustandes des angegriffenen Systems, Auslösen einer &lt;a href="https://en.wikipedia.org/wiki/Race_condition" target="_blank" rel="noopener noreferrer" class="external-link"&gt;race condition&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; etc.), ohne zusätzliche Rechte (als Gast ohne Authentisierung oder Autorisierung) oder Interaktion mit dem User oder der Administratorin des Systems durchgeführt werden kann.&lt;/p&gt;&#10;&lt;p&gt;Das wäre für sich genommen noch nicht schlimm, wenn niemand das System benötigt &lt;code&gt;Score 0.0&lt;/code&gt; und nichts Interessantes darauf gespeichert ist. Liegen dort allerdings sensitive Daten &lt;code&gt;Confidentiality, 8.7&lt;/code&gt; oder zum Beispiel eine Datenbank, deren Werte sich nun manipulieren lassen &lt;code&gt;Integrity, 8.7&lt;/code&gt;, oder wird auch nur die Verfügbarkeit von benötigten Daten eingeschränkt &lt;code&gt;Availability, 8.7&lt;/code&gt;, befindet man sich bereits im Bereich &lt;code&gt;High&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Mit diesem Wissen nehmen wir nun den Transfer auf die kritischen Lücken von Gitea vor.&lt;/p&gt;&#10;&lt;h3 id="cve-2026-20896-authentication-bug-in-gitea-through-reverse-proxies"&gt;CVE-2026-20896: Authentication bug in Gitea through reverse-proxies&lt;/h3&gt;&#10;&lt;p&gt;Kommt ein Angreifer hinter den Reverse Proxy (oder ist gar kein Reverse Proxy vorgeschaltet), kann er eine Anfrage mit &lt;code&gt;X-WEBAUTH-USER&lt;/code&gt; im Header direkt an die Gitea-Instanz senden und sich für einen Reverse Proxy ausgeben. Ist Gitea gleichzeitig darauf eingestellt, &lt;a href="https://app.opencve.io/cve/CVE-2026-20896" target="_blank" rel="noopener noreferrer" class="external-link"&gt;allen reverse proxies zu vertrauen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; &lt;code&gt;REVERSE_PROXY_TRUSTED_PROXIES = *&lt;/code&gt;, so geht es davon aus, dass bereits &lt;a href="https://df00tech.com/detections/blog/cve-2026-20896" target="_blank" rel="noopener noreferrer" class="external-link"&gt;authentisiert wurde und eine gültige Session vorliegt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Der Angreifer erhält also Nutzerrechte mit Schreibzugriff auf Repositories, obwohl er nicht mal einen Account angelegt hat.&lt;/p&gt;&#10;&lt;p&gt;Das eigentliche Lücke ist hier das unhinterfragte Vertrauen gegenüber dem Sender des Headers ohne weiteren Abgleich mit einem Session Token oder ähnlichem; das Annehmen desselben von fremden Quellen erzeugt lediglich die Verwundbarkeit.&lt;/p&gt;&#10;&lt;p&gt;Für diesen Bug gibt es &lt;a href="https://www.cloudlinktech.com/news/active-attacks-exploit-critical-gitea-docker-auth-bug/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Belege für bereits erfolgte Attacken&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="cve-2026-59774-file-read-flaw-for-anything-the-gitea-service-account-can-read"&gt;CVE-2026-59774: File-read flaw for anything the Gitea Service Account can read&lt;/h3&gt;&#10;&lt;p&gt;Diese kritische Sicherheitslücke basiert auf einer unzureichenden Eingabeprüfung des &lt;a href="https://docs.gitea.com/next/administration/external-renderers/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;eingebauten Textrenderers von Gitea&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für &lt;a href="https://en.wikipedia.org/wiki/Org-mode" target="_blank" rel="noopener noreferrer" class="external-link"&gt;org-mode&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Dateien. Eine detaillierte Beschreibung des Vektors und zu möglichen Wegen eines Angriffs kann auf &lt;a href="https://www.satyamrastogi.com/blog/gitea-org-mode-unauthenticated-file-read-cve-2026-59774/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Saytam Rastogis&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; hervorragendem Blog nachgelesen werden.&lt;/p&gt;&#10;&lt;p&gt;Kurzgesagt kann sich ein Angreifer mit Zugriff auf zumindest ein öffentliches Repository eine bösartige &lt;code&gt;.org&lt;/code&gt;-Datei zusammenbasteln, mit deren Hilfe er Giteas Konfiguration &lt;code&gt;app.ini&lt;/code&gt; auslesen und damit auf darin enthaltene Secrets zugreifen kann, welche ihm erweiterte Rechte zugestehen oder ihn zumindest lateral auf dem System bewegen lassen.&lt;/p&gt;&#10;&lt;p&gt;Dies kann laut &lt;a href="https://thehackernews.com/2026/08/critical-gitea-flaw-let-unauthenticated.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hacker news&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und &lt;a href="https://github.com/go-gitea/gitea/security/advisories/GHSA-6v53-hr58-556r" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Giteas eigenen Leuten&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zufolge beispielsweise auch dazu ausgenutzt werden, die Registration Token eines Runners auszulesen und selbigen zu übernehmen.&lt;/p&gt;&#10;&lt;h3 id="cve-2026-60004-rce-allowing-attacker-with-ordinary-user-rights-to-execute-arbitrary-shell-commands-as-the-gitea-os-user"&gt;CVE-2026-60004: RCE allowing attacker with ordinary user rights to execute arbitrary shell commands as the Gitea OS user.&lt;/h3&gt;&#10;&lt;p&gt;Diese Sicherheitslücke ist deswegen besonders schlimm, weil sie zusätzlich eine &lt;a href="https://www.thehackerwire.com/vulnerability/CVE-2026-60004/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;privilege escalation ermöglicht&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;The vulnerability in question is CVE-2026-60004 (CVSS score: 9.8), a case of remote code execution that allows an attacker with ordinary write access to a repository to execute arbitrary shell commands as the Gitea OS user.&amp;rdquo; &lt;a href="https://thehackernews.com/2026/08/critical-gitea-rce-actively-exploited.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;thehackernews&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, Aug-2026&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Das Ausnutzen dieser Lücke kann den Angreifer also die gesamte Gitea-Instanz übernehmen lassen, sofern er Schreibzugriff auf irgendein (evtl. sogar öffentliches) Repository hat.&lt;/p&gt;&#10;&lt;h2 id="das-lagebild"&gt;Das Lagebild&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-10-04-gitea-cve-chaining.avif" alt="Image: A decision tree, connecting the dots between the three discussed CVEs. Baseline message is that the system has to be regarded as compromised in case an attacker is able to get ordinary write access to any repository on the instance which can be done by exploiting any of the discussed CVEs."&gt;&lt;/figure&gt;&#10;Es gibt noch weitere kritische Sicherheitslücken, allerdings könen sich Angreifer mit einer Kombination der obigen Darstellungen ohne großen Aufwand&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Als normale User auf dem Gitea-System bewegen, &lt;em&gt;ohne sich registriert zu haben&lt;/em&gt;, und &lt;em&gt;ohne SSH-Zugriff&lt;/em&gt; zu benötigen&lt;/li&gt;&#10;&lt;li&gt;Sich als Gitea-Service User ausgeben&lt;/li&gt;&#10;&lt;li&gt;Actions übernehmen oder sogar Workflows injizieren und ausführen&lt;/li&gt;&#10;&lt;li&gt;Mit etwas Mehraufwand die gesamte Instanz übernehmen, verschlüsseln, Lösegeld verlangen usw.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Was mein Bekannter erlebt hat betraf die Punkte eins und drei. Der Angriff wurde sehr wahrscheinlich automatisiert ausgeführt, nachdem das Internet auf verwundbare Instanzen gescannt worden war.&lt;/p&gt;&#10;&lt;p&gt;Zusammengefasst kann man im vorliegenden Fall von einer &lt;a href="https://www.techtimes.com/articles/325724/20260827/cisa-confirms-gitea-cve-2026-60004-exploited-cryptominer-hits-5000-exposed-dev-servers.htm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Mass-Exploitation Campaign&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für Cryptomining ausgehen.&lt;/p&gt;&#10;&lt;h3 id="der-cryptominer"&gt;Der Cryptominer&lt;/h3&gt;&#10;&lt;p&gt;Bei meinem Bekannten wird folgende Datei als statisch gelinkte Executable (!) auf dem System abgelegt: &lt;code&gt;gitea/data/gitea/sys_health_s3&lt;/code&gt;&#10;Der Hash der Datei legt seiner Ansicht nach nahe, dass ein &lt;a href="https://github.com/xmrig/xmrig" target="_blank" rel="noopener noreferrer" class="external-link"&gt;XMRig cyptominer&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; als &lt;a href="https://www.cloudflare.com/learning/security/glossary/malicious-payload/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;bösartige Payload&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; eingesetzt wurde, der mit &lt;code&gt;pool.hashvault.pro&lt;/code&gt; kommuniziert und dabei einen Großteil der verfügbaren CPU-Leistung für sich beansprucht.&lt;/p&gt;&#10;&lt;h3 id="noch-größerer-schaden-möglich"&gt;Noch größerer Schaden möglich&lt;/h3&gt;&#10;&lt;p&gt;Sogar für den auslegungsüberschreitenden Stöfall habe ich &lt;a href="https://thehackernews.com/2026/09/red-heron-exploits-gitea-rce-to.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ein aktuelles (Sep-2026) Beispiel auf Hackernews&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; finden können: Diebstahl von Quellcode, Sammlung von Zugangsdaten, Privilege escalation bis hin zum &lt;code&gt;root&lt;/code&gt;-Zugriff auf das betroffene System.&lt;/p&gt;&#10;&lt;h2 id="wie-entdecke-ich-einen-angriff"&gt;Wie entdecke ich einen Angriff?&lt;/h2&gt;&#10;&lt;p&gt;Schlimmstenfalls gar nicht. Sind Profis zielgerichtet am Werk, können allenfalls die Logdateien zum Zeitpunkt direkt nach dem Einbruch Aufschluss geben. Im Falle, dass man sie sofort liest und interpretieren kann. Aber ich schweife ab&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#10;&lt;p&gt;Dannoch sollte man sich folgende Fragen stellen:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Betreibe ich eine vulnerable Gitea-Version (&lt;code&gt;&amp;lt;1.27.2&lt;/code&gt;)?&lt;/li&gt;&#10;&lt;li&gt;Verzichte ich auf eine Monitoring-Lösung, die bei Auffälligkeiten warnt?&lt;/li&gt;&#10;&lt;li&gt;Liegt eine ungewöhnlich &lt;em&gt;hohe CPU-Last&lt;/em&gt; des Systems vor (Deutet auf Kryptominer-Befall hin)?&lt;/li&gt;&#10;&lt;li&gt;Für diesen konkreten Miner kann man zusätzlich nachsehen, ob auf dem System folgende Datei zu finden ist:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;find / -name &lt;span style="color:#e6db74"&gt;&amp;#39;.sys_health_s3&amp;#39;&lt;/span&gt; 2&amp;gt;/dev/null&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Beantwortet man sich nur eine dieser Fragen mit &lt;code&gt;ja&lt;/code&gt;, ist zügiges Handeln angebracht. Dabei ist durch den Schweregrad der Sicherheitslücken das gesamte Hostsystem als kompromittiert anzusehen, insbesondere wenn der Gitea-Container oder ein eventuell eingebundener Runner &lt;code&gt;rootful&lt;/code&gt; läuft, also mit dem Docker Socket ausgestattet wurde.&lt;/p&gt;&#10;&lt;h3 id="bei-befall-quarantäne"&gt;Bei Befall: Quarantäne&lt;/h3&gt;&#10;&lt;p&gt;Nächste Schritte: Sofort die betroffene Maschine isolieren und vom Netz nehmen. Die Container herunterfahren, Hostmaschine durchbooten und Updates nachladen. Aschließend die Docker Registry auf Stand bringen und über die Logdateien (Zeitstempel!) ermitteln, ob andere Teile des Systems betroffen sind. Für weitere Details siehe &lt;a href="https://blog.schallbert.de/gitea-out-of-memory/"&gt;mein Artikel über einen selbsterlebten Angriff auf Gitea&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="lösungen"&gt;Lösungen&lt;/h2&gt;&#10;&lt;h3 id="gitea-updaten"&gt;Gitea Updaten!&lt;/h3&gt;&#10;&lt;p&gt;Gitea muss dringend auf eine Version &lt;code&gt;&amp;gt;1.27.1&lt;/code&gt; gebracht werden. Dafür kann beispielsweise die &lt;code&gt;docker-compose.yml&lt;/code&gt; geändert und die Container danach durchgebootet werden:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;gitea&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/gitea:latest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Weitere Informationen zu aktuellen Fixes finden sich auf &lt;a href="https://blog.gitea.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Giteas Blog&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Der Major Release auf Version 27 löst eine ellenlange Liste an kritischen Sicherheitslücken. Ich habe in diesem Artikel nur ein paar davon herausgepickt, da sie mein direktes Umfeld betreffen und ihre Ausnutzung bekannt ist.&lt;/p&gt;&#10;&lt;h3 id="gitea-und-runner-rootless-ausführen"&gt;Gitea und Runner &lt;code&gt;rootless&lt;/code&gt; ausführen&lt;/h3&gt;&#10;&lt;p&gt;Führt das Ausnutzen einer Sicherheitslücke zum Verlust von Gitea oder dem Action Runner, sollte nicht auch noch das Hostsystem kompromittiert werden. Der Betrieb in einem Container kann den Schadensumfang deutlich reduzieren. Das Aufsetzen einer &lt;a href="https://blog.schallbert.de/gitea-act-runner-dind/"&gt;&lt;code&gt;rootless&lt;/code&gt;-Lösung habe ich daher hier beschrieben&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="zusätzliche-maßnahmen-härtung"&gt;Zusätzliche Maßnahmen (Härtung)&lt;/h2&gt;&#10;&lt;h3 id="cve-2026-20896-netzwerkzugriff-erschweren"&gt;CVE-2026-20896: Netzwerkzugriff erschweren&lt;/h3&gt;&#10;&lt;p&gt;Bei den folgenden Schritten folge ich der &lt;a href="https://df00tech.com/detections/CVE-2026-20896#kql" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Empfehlung von d00ftech&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Einfügen des Adressraumes des selbst verwendeten Reverse Proxy. Dafür gilt erst einmal herauszufinden, unter welcher Adresse der Reverse Proxy im Netzwerk erreichbar ist. Auf meinem Server beispielsweise laufen Gitea und Reverse Proxy (Caddy) in Docker. Daher finde ich die IP-Adresse wie folgt:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker inspect caddy &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --format&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;{{range $name, $network := .NetworkSettings.Networks}}{{$name}}: {{$network.IPAddress}}{{&amp;#34;\n&amp;#34;}}{{end}}&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;caddy-proxy: 172.20.0.2&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dieser Wert wird nun in der &lt;code&gt;gitea/app.ini&lt;/code&gt; eingetragen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[security]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REVERSE_PROXY_LIMIT&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REVERSE_PROXY_TRUSTED_PROXIES&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;172.20.0.2/32&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="2"&gt;&#10;&lt;li&gt;Direkten Netzwerkzugriff auf Gitea unter Umgehung des eigenen Reverse Proxy blockieren. Dazu darf der Port von Gitea (meist &lt;code&gt;3000&lt;/code&gt;) nicht in der Portfreigabe beispielsweise der Docker-Konfiguration erscheinen:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;services&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;gitea&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ports&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;3000:3000&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# remove this!&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="cve-2026-59774-gitea-updaten"&gt;CVE-2026-59774: Gitea updaten!&lt;/h3&gt;&#10;&lt;p&gt;Es gibt zwar die Möglichkeit, den org-mode renderer in der &lt;code&gt;app.ini&lt;/code&gt; zu deaktivieren (Siehe &lt;a href="https://docs.gitea.com/next/administration/external-renderers/#appini-file-configuration" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea Documentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zum Thema), der weitaus bessere Weg ist aus meiner Sicht aber das Update von Gitea selbst!&lt;/p&gt;&#10;&lt;h3 id="cve-2026-60004-falls-möglich-self-registration-abschalten"&gt;CVE-2026-60004: Falls möglich Self-Registration abschalten&lt;/h3&gt;&#10;&lt;p&gt;Der eigentliche Fix ist nur durch ein Update der Gitea-Version möglich. Für kleinere Instanzen mit wenigen Usern mag es zusätzlich zweckmäßig erscheinen, die Selbstregistrierung für neue User abzuschalten. Sie müssen eine Registrierung von nun an bei den Administratoren anfragen (sofern sie nicht eine der anderen oben beschriebenen CVEs ausnutzen).&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/app.ini&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[service]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DISABLE_REGISTRATION&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Weitere hilfreiche Einstellungen sind:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REGISTER_EMAIL_CONFIRM&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true # Requires user&amp;#39;s email to be confirmed for registration to complete&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLE_OPENID_SIGNUP&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false # Disallow logons via external API&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REQUIRE_SIGNIN_VIEW&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true # Force users to login before they can view content or use the public API &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="ausblick-und-fazit"&gt;Ausblick und Fazit&lt;/h2&gt;&#10;&lt;p&gt;Versionsverwaltungen sind sehr atttraktive Ziele für gleich einen ganzen Strauß an Angreifertypen: Von Cryptominern wegen der Möglichkeit der Ressourcenübernahme über Ransomware-Gruppen wegen der Quellcodedateien und möglicher Zugangsdaten bis hin zu hochprofessionellen, zielgerichteten Angreifern und staatlichen Akteuren dadurch, dass die Software eine Art Infrastruktur darstellt.&lt;/p&gt;&#10;&lt;p&gt;Daher:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Alle Patches einspielen. Immer. Sofort. Ohne Ausnahmen.&amp;rdquo; - &lt;a href="https://codeberg.org/tomas-jakobs/fefe-blog-backup/src/branch/main/content/assets/hackback/hackback.htm#L509" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Felix von Leitner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, sagt aber auch das &lt;a href="https://www.bsi.bund.de/DE/Themen/Verbraucherinnen-und-Verbraucher/Informationen-und-Empfehlungen/Cyber-Sicherheitsempfehlungen/Updates-Browser-Open-Source-Software/Wichtige-Softwareupdates/wichtige-softwareupdates_node.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;BSI&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Was mich ein wenig ratlos zurücklässt ist die schiere Zahl und Kritikalität der gestopften Sicherheitslücken (35 CVEs binnen eines Monats laut Giteas eigenem Blog!). Aus meiner blauäugigen Sicherheitsperspektive ist diese Lösung ein Schweizer Käse und ich bin mir nicht sicher, ob ich Gitea weiter betreiben sollte.&lt;/p&gt;&#10;&lt;h3 id="sofort-härtung-durchführen"&gt;Sofort: Härtung durchführen&lt;/h3&gt;&#10;&lt;p&gt;Na klar: Gitea auf &lt;code&gt;latest&lt;/code&gt; bringen und die oben beschriebenen Härtungsmaßnahmen durchführen.&lt;/p&gt;&#10;&lt;h3 id="kurzfristig-automatische-updates"&gt;Kurzfristig: Automatische Updates&lt;/h3&gt;&#10;&lt;p&gt;In den nächsten Tagen und Wochen werde ich mich damit beschäftigen, die Docker Registry automatisch nach Updates für meine Software zu durchsuchen und sie ebenso automatisch auf meinem Server auszurollen. &lt;a href="https://blog.schallbert.de/auto-upgrade-docker-images/"&gt;Blog-Post dazu hier&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="mittelfristig-her-mit-monitoring"&gt;Mittelfristig: Her mit Monitoring&lt;/h3&gt;&#10;&lt;p&gt;Ich werde mich auf die Suche machen nach einer kleinen und leichten Monitoring-Lösung, die Auffälligkeiten wie &amp;ldquo;CPU-Last ungewöhnlich hoch seit HH:MM:SS&amp;rdquo; meldet und &lt;a href="https://oneuptime.com/blog/post/2026-01-06-docker-health-checks/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;healthchecks&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; durchführen und überwachen kann. Dabei darf sie selbst kein Einfallstor dastellen, muss also praktisch ohne Rechte auskommen. Ob das wohl einfach möglich ist?&lt;/p&gt;&#10;&lt;h3 id="mittelfristig-2-weg-mit-den-secrets"&gt;Mittelfristig 2: Weg mit den Secrets&lt;/h3&gt;&#10;&lt;p&gt;Unverschlüsselte Secrets haben in Repositories nichts verloren. Selbst auf dem Server selbst ist eine &lt;code&gt;.env&lt;/code&gt;-Datei, die dort herumliegt, als nicht mehr sicher zu werten. Auch hier werde ich mir Gedanken machen und entsprechend berichten, wenn ich eine Lösung ausgerollt habe.&lt;/p&gt;&#10;&lt;h3 id="mittelfristig-3-news-feeds-einrichten"&gt;Mittelfristig 3: News-Feeds einrichten&lt;/h3&gt;&#10;&lt;p&gt;Hierunter fallen Abos z.B. von Newslettern oder RSS-Feeds idealerweise aller meiner Softwareanbieter, IT-Security-Institutionen oder -Journalisten und weiteren Quellen, aus denen ich Sicherheitslücken, Patches und Maßnahmen zur Systemhärtung entnehmen kann.&lt;/p&gt;&#10;&lt;h3 id="credits"&gt;Langfristig: Mit Leuten weiterhin ins Gespräch gehen&lt;/h3&gt;&#10;&lt;p&gt;Es endet wie es beginnt. Bevor ich Probleme mit meiner Software lösen kann, bevor ich überhaupt von den Problemen weiß, ja genau da! Ganz am Anfang, bevor ich überhaupt entschieden habe, welche Software ich einsetze, da stehen: Andere Menschen.&lt;/p&gt;&#10;&lt;p&gt;Leute, die ich fragen kann, um Hilfe bitten kann. Deren Zeit ich in Anspruch nehmen darf für eigene Belange. Die ich zum Beispiel per Messenger aus dem Nichts heraus anschreibe und aus ihrem Fokus reiße.&lt;/p&gt;&#10;&lt;p&gt;Leute, die sich besser auskennen als ich, die einen anderne Blick auf die Dinge haben, oder deren Meinung ich interessant finde.&lt;/p&gt;&#10;&lt;p&gt;Kolleginnen, auch Ehemalige, Bekannte und Freunde, Familie, die ich zufällig oder zielgerichtet auf einen Austausch treffe. Auf einen Kaffee, ein Eis, ein Kaltgetränk, zum Musizieren oder für einen Brettspielnachmittag.&lt;/p&gt;&#10;&lt;p&gt;Leute, die mir trotz der Unterbrechung und doofer Fragen freundlich und geduldig helfen. Die vielleicht kurz mit den Augen rollen, mir die Dinge dann aber doch noch ein weiteres mal erklären. Die meine Monologe, kritische Nachfragen und gelegentliche Gegenrede irgendwie tolerieren.&lt;/p&gt;&#10;&lt;p&gt;Leute, mit denen ich in einen Austausch treten kann, der am Ende hoffentlich beiden Seiten nützt.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/root/Schallbert.avif" alt="Image: A computer-drawn image of Schallbert, pixelized, and in 256 colors."&gt;&lt;/figure&gt;&#10;Bei euch allen möchte ich mich bedanken. Mit Leuten wie euch wird alles viel einfacher. Auch der Betrieb dieses Blogs, wo so viel meiner Zeit hineingeht. Ich kann mit euch Entscheidungen diskutieren, erfahren wie ihr Dinge gemacht habt und warum genau so, die Perspektiven wechseln. Und neue Ideen entwickeln.&lt;/p&gt;&#10;&lt;p&gt;Bitte bleibt mir erhalten.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Verweis an &lt;a href="https://de.wikipedia.org/wiki/Fefes_Blog" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Fefe&amp;rdquo; Felix von Leitner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Zitat: &amp;ldquo;Annahmen [&amp;hellip;]: Wir werden angegriffen. Wir merken es zeitnah. Aller Erfahrung nach: Nein, tun wir nicht. Typischer Zeitraum vor Entdeckung von blinden Passagieren: Ein halbes bis fünf Jahre.&amp;rdquo; - aus dem Vortrag &lt;a href="https://codeberg.org/tomas-jakobs/fefe-blog-backup/src/commit/5d0e24d085463a5fc3b852edb589347f6d909a85/content/assets/hackback/hackback.htm#L634" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hackback&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, Link führt zu &lt;a href="https://codeberg.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Codeberg&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Hugo vs. Jekyll: Benchmark</title><link>https://blog.schallbert.de/hugo-versus-jekyll-benchmark/</link><pubDate>Wed, 30 Sep 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/hugo-versus-jekyll-benchmark/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-09-30-hugo-jekyll-benchmark.avif"&#10; class="post-cover"&#10; alt="Image: The result of a page load time and page bandwidth test in the browser. There is a cake diagram, showing the majority of data transfer accounting for images, followed by css with a fifth. HTML content is the smallest part with a tenth or less."&#10; title="Hugo vs. Jekyll: Benchmark" /&gt;&#10;&lt;h2 id="ladezeiten"&gt;Ladezeiten&lt;/h2&gt;&#10;&lt;p&gt;Schauen wir uns mal an, wie meine Seite mit Jekyll gegenüber Hugo lädt. Hierfür öffne ich die &lt;em&gt;Entwickleroptionen (F12)&lt;/em&gt; in &lt;a href="https://www.firefox.com/de/?redirect_source=mozilla-org" target="_blank" rel="noopener noreferrer" class="external-link"&gt;meinem Browser&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, wähle das &lt;em&gt;Network&lt;/em&gt;-Tab an und klicke dort auf &lt;em&gt;All&lt;/em&gt;. Anschließend lade ich die Seite neu.&lt;/p&gt;&#10;&lt;p&gt;Dabei entsteht die folgende Auflistung:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/migrating-hugo-to-jekyll/blog_schallbert-transfersize-loadtime-hugo.avif" alt="Image: A snapshot of my browser window with the blog&amp;#39;s landing page opened. Developer options are activated, and the lower part of the image shows a table with the individual page content files, their size, and contribution to the load time. For hugo, it transfers a total of 189kB in 175ms."&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Im Bild unten: Ladezeit meiner Landing Page mit Hugo&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;Zu erwähnen ist, dass ich hier eigentlich gar nicht Jekyll mit Hugo vergleiche, sondern lediglich deren Ausgabe und daher die gewählten Themes maßgeblich für Größe und Ladezeit verantwortlich sind. Also vergleiche ich praktisch das &lt;em&gt;minimalmistakes theme in Jekyll&lt;/em&gt; mit dem &lt;em&gt;terminal theme in Hugo&lt;/em&gt;. Hugo ist in diesem Falle etwa &lt;code&gt;10%&lt;/code&gt; schneller als Jekyll.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/migrating-hugo-to-jekyll/blog_schallbert-transfersize-loadtime-jekyll.avif" alt="Image: A snapshot of my browser window with the blog&amp;#39;s landing page opened. Developer options are activated, and the lower part of the image shows a table with the individual page content files, their size, and contribution to the load time. For jekyll, it transfers a total of 328kB in 306ms."&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Im Bild unten: Ladezeit meiner Landing Page mit Jekyll&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="größe"&gt;Größe&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/migrating-hugo-to-jekyll/blog_schallbert-landing-performance-analysis-terminaltheme.avif" alt="Image: A cake diagram as the output of firefox&amp;#39;s performance analysis tool in developer options. Analyzing the hugo&amp;#39;s terminal theme on a typical blog post of mine, images make up for three quarters of total transferred size. The last quarter is largely taken by css, and just 12kB html at a total size of 165kB."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Ein anderes Diagramm erhalte ich, wenn ich ganz unten links auf die Stoppuhr klicke und damit eine &lt;a href="https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/performance_analysis/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;performance analysis&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ausführe. Die Kuchendiagramme zeigen für meine Implementierung in Jekyll den größten Anteil in Bildern, gefolgt von CSS und JS. In Hugo geht JS gegen null, und auch der Anteil von CSS ist geschrumpft.&lt;/p&gt;&#10;&lt;p&gt;Die folgende Tabelle zeigt ein paar meiner Seiten im direkten Vergleich. Allerdings ist die Übersicht mit Vorsicht zu genießen: Die Werte ändern sich schon mal, wenn ich neu lade (auch mit ausgeschaltetem Cache). Gerade bei eingebundenen Inhalten Dritter wie meinen Videos kommen hier teils stark unterschiedliche Werte heraus. Insgesamt habe ich mit Hugo und Terminal deutliche Optimierungen vorliegen.&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Item&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;minimal-mistakes&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;terminal-schallbert&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Jekyll&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Hugo&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;transferred&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[kB]&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Diff [%]&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;time&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[ms]&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Diff [%]&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Landing&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;163.47&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;98.68&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-39&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Post&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2861&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1767&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-38&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;340&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;270&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-20&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;List page&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1507&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;140&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-90&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Manuell implementiert in Hugo hatte ich das bedingte Laden von Katex nur auf Seiten, wo mathematische Ausdrücke auftauchen (das spart oftmals &lt;code&gt;90kB min.css&lt;/code&gt;).&lt;/li&gt;&#10;&lt;li&gt;Mit Hugo habe ich kleinere CSS-Dateien. Es wird nur geladen, was ich auch wirklich verwende. Es gibt kaum noch Code, der nicht angefahren wird.&lt;/li&gt;&#10;&lt;li&gt;Automatisch in Hugo sind Medien-Vorschaudateien von den hochauflösenden Dateien abgeleitet und per Bildprozessor auf die Darstellungsverhältnisse im gebauten Zustand platzoptimiert. Diese Optimierung spart bei vielen, klein dargestellten Bildern eine Menge Bandbreite.&lt;/li&gt;&#10;&lt;li&gt;Das &lt;em&gt;Terminal&lt;/em&gt;-Theme zeigt anders als &lt;em&gt;minimalmistakes&lt;/em&gt; keine Kärtchen für &amp;ldquo;weitere Posts&amp;rdquo; an. Dies spart die Übertragung von vier Vorschaubildern.&lt;/li&gt;&#10;&lt;li&gt;In Hugo habe ich fast gar keine Javascript-Inhalte mehr - eines der Ziele, das ich mir gesteckt hatte.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="auslieferungcontinuous-deployment"&gt;Auslieferung/Continuous Deployment&lt;/h2&gt;&#10;&lt;p&gt;Für Jekyll gestaltete sich Bauen und Ausliefern in Gitea etwas hakelig: erst mit einem &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/"&gt;Jekyll-Dockerimage&lt;/a&gt; war ich in der Lage, &lt;a href="https://blog.schallbert.de/gitea-action-runner-native-jekyll/"&gt;ruby richtig aufzusetzen&lt;/a&gt;. Damit und weil ich meinen Actions nicht erlaube zu cachen dauert das Bauen ziemlich lang: Erst den aktuellen Docker-Container ziehen, ruby und jekyll updaten, gems installieren, die Quelldateien auschecken, anschließend bauen und ausliefern - hier vergehen selbst unter optimalen Bedingungen mehr als &lt;code&gt;90 Sekunden&lt;/code&gt;, bis ein neuer Artikel live ist.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-09-30-CD-build-with-jekyll.avif" alt="Image: Gitea dashboard in the Actions tab. We see three successful workflow runs with a hugo full build &amp;amp; deploy of my site, taking 122sec, 138sec, and 160sec."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Für Hugo geht der ganze Prozess dank schnellerer Bauzeiten und vorgefertigter Container (siehe dazu mein Artikel zum Thema &lt;a href="https://blog.schallbert.de/build-deploy-hugo-with-actions-docker-caddy/"&gt;Continuous Deployment Pipeline mit Hugo in Docker&lt;/a&gt;) viel schneller vonstatten.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-09-30-CD-build-with-hugo.avif" alt="Image: Gitea dashboard in the Actions tab. We see three successful workflow runs with a hugo full build &amp;amp; deploy of my site, taking 12sec, 9sec, and 21sec."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Selbst im ungünstigsten Fall (siehe Build unten), z.B. nach einem Neustart des Runners nach Update und einer Änderung im Docker-Repository für meinen Hugo Container, bleibe ich weit unter &lt;code&gt;30 Sekunden&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h2 id="komplexität"&gt;Komplexität&lt;/h2&gt;&#10;&lt;p&gt;Ganz klar: Hugo ist hier im Vergleich zu Jekyll unschlagbar.&lt;/p&gt;&#10;&lt;p&gt;Da Themes in Hugo steckbare Module sind, benötigen sie keinen Abhängigkeitsbaum in Form von Gems wie bei Jekyll zum Nachladen und installieren. Entsprechend kann hier viel weniger kaputt gehen. Für mich lokal verringert sich die Bauzeit für einen &amp;ldquo;full build&amp;rdquo; (zweisprachig) um &lt;strong&gt;75%&lt;/strong&gt; und auch das automatische Bauen bei Veränderung meiner Markdown-Dateien ist sowohl &lt;strong&gt;5x&lt;/strong&gt; schneller als auch aussagekräftiger: Wenn ich in Jekyll nämlich &amp;ldquo;incremental build&amp;rdquo; einschalte, dann gibt es keine aktualisieren Paginator-Seiten mehr - sprich Listenansichten und Übersichtsseiten werden nicht mit gebaut.&lt;/p&gt;&#10;&lt;p&gt;Auf der anderen Seite kann ich in Jekyll sehr flexibel und feingranular selbst eingreifen, eigene Gems entwerfen und recht einfach besondere Gimmicks für meine Seite herstellen wie ein mitscrollendes Inhaltsverzeichnis. Jenes vermisse ich in Hugo etwas.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Fazit: Für die tagtägliche Benutzbarkeit gefällt mir Hugo ganz klar besser als Jekyll. - &lt;em&gt;schallbert, Sep-2026&lt;/em&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;</description></item><item><title>Continuous Deployment für Hugo</title><link>https://blog.schallbert.de/build-deploy-hugo-with-actions-docker-caddy/</link><pubDate>Tue, 15 Sep 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/build-deploy-hugo-with-actions-docker-caddy/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-09-15-dind-action-setup-cover.avif"&#10; class="post-cover"&#10; alt="Image: A hand-drawn Docker container layout image. It shows the runner container on a VM with a debian OS, which starts an action container, which in turn runs the hugo deployment container."&#10; title="Continuous Deployment für Hugo" /&gt;&#10;&lt;p&gt;Gerade habe ich meine Website &lt;a href="https://blog.schallbert.de/projects/migrating-jekyll-to-hugo/"&gt;von Hugo auf Jekyll umgezogen&lt;/a&gt;. Nun will ich sie automatisiert bauen und Änderungen ausrollen, sobald ich einen Artikel fertiggestellt und ihn per &lt;code&gt;git push&lt;/code&gt; und anschließendem Merge in &lt;code&gt;main&lt;/code&gt; auf meine &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/"&gt;Gitea-Instanz&lt;/a&gt; geschoben habe.&lt;/p&gt;&#10;&lt;h2 id="die-befehlskette"&gt;Die Befehlskette&lt;/h2&gt;&#10;&lt;p&gt;Ich habe einen &lt;a href="https://en.wikipedia.org/wiki/Virtual_private_server" target="_blank" rel="noopener noreferrer" class="external-link"&gt;VPS&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gemietet, auf dem ich alle Anwendungen in Docker laufen lasse. Die Aufgabe ist nun, einen &lt;a href="https://docs.github.com/en/actions/concepts/workflows-and-actions/workflows" target="_blank" rel="noopener noreferrer" class="external-link"&gt;workflow&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; in Gitea zu bauen (&amp;ldquo;Actions&amp;rdquo;), welcher das Update im Repository auscheckt, es &lt;a href="https://hub.docker.com/r/hugomods/hugo" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hugo&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Verfügung stellt, dort die HTML-Seiten erzeugen lässt, und sie anschließend so auf der Festplatte ablegt, dass mein Webserver die Dateien einlesen und ausliefern kann.&lt;/p&gt;&#10;&lt;h3 id="erfahrungswerte"&gt;Erfahrungswerte&lt;/h3&gt;&#10;&lt;p&gt;Das alles hatte ich für meine &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/"&gt;Website in Jekyll bauen und ausrollen&lt;/a&gt; können, daher kann ich auf ein paar Erfahrungswerte zurückgreifen.&lt;/p&gt;&#10;&lt;p&gt;Üblicherweise nutzt man dafür standardisierte Aktionen. Ich vereinfache mal stark und liste nur die Hauptdarsteller auf, ohne in fiese Details wie Rechtevergabe (darf Hugo in diesen Ordner schreiben? Hat der hugo-user genügend Rechte?) tiefer einzugehen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/actions/checkout" target="_blank" rel="noopener noreferrer" class="external-link"&gt;checkout&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; stellt einen gewünschten Stand des Quellrepository bereit&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/peaceiris/actions-hugo" target="_blank" rel="noopener noreferrer" class="external-link"&gt;actions-hugo&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; installiert hugo in der virtuellen Maschine der Action, sodass die Website gebaut werden kann&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/actions/upload-artifact" target="_blank" rel="noopener noreferrer" class="external-link"&gt;upload-artifact&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; lädt die von hugo erstellte Seite auf den Webserver hoch&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Da ich jedoch auf ein- und derselben Maschine in Docker unterwegs bin, kann ich mir das aufwendige Einwählen auf dem Webserver &amp;ldquo;von außen&amp;rdquo; sparen. Daher gehe ich einen anderen Weg.&lt;/p&gt;&#10;&lt;h2 id="docker-shared-volumes"&gt;Docker shared Volumes&lt;/h2&gt;&#10;&lt;p&gt;Ich kann meiner &lt;em&gt;Caddy&lt;/em&gt;-Anwendung direkt die gebauten Dateien zur Verfügung stellen, wenn ich das von ihr gelesene Volume bis in die Action weiterreiche.&lt;/p&gt;&#10;&lt;h3 id="aufgepasst-action-volume--hugo-volume"&gt;Aufgepasst: Action volume != Hugo volume&lt;/h3&gt;&#10;&lt;p&gt;Doch das funktioniert mit &lt;em&gt;actions-hugo&lt;/em&gt; nicht: Sie ist selbst kein Docker container, kann also keine Volumes einbinden. Sie baut zwar die Webseite fertig; sobald die Aktion jedoch durchgelaufen ist, reißt sie die Artefakte mit ins Nirvana.&lt;/p&gt;&#10;&lt;p&gt;Ich benötige also eine hugo-Runtime, die selbst in einem Docker-Container läuft - ganz nach dem &lt;a href="https://blog.schallbert.de/gitea-act-runner-dind/"&gt;DinD-Ansatz&lt;/a&gt;. Diesem kann ich dann das von Caddy verwendete Volume mit Schreibzugriff durchreichen.&lt;/p&gt;&#10;&lt;h2 id="der-workflow"&gt;Der Workflow&lt;/h2&gt;&#10;&lt;p&gt;Doch welchen Docker-Container soll ich nehmen?&lt;/p&gt;&#10;&lt;p&gt;Man stelle sich nun mehrere Stunden der Recherche und viele Fehlversuche beim Einrichten des Workflows auf meinem Server vor.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-09-15-hugo-unsuccessful-workflow-run-history.avif" alt="Image: Giteas workflow run overview. We see many actions at the bottom with a red &amp;#39;build failed&amp;#39;-flag. Only the top two workflow runs were successful."&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;Folgendes ergab sich aus meinen Untersuchungen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;checkout@v4&lt;/code&gt; benötigt &lt;a href="https://nodejs.org/en" target="_blank" rel="noopener noreferrer" class="external-link"&gt;node.js&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, und zwar mindestens Version &lt;em&gt;node:18&lt;/em&gt;. Der Docker-Container (und das darunterliegende Betriebssystem des Runners!) müssen es installiert haben bzw. unterstützen.&lt;/li&gt;&#10;&lt;li&gt;Die Action benötigt &lt;code&gt;git&lt;/code&gt;. Auch dies muss der Docker-container idealerweise bereits mitbringen.&lt;/li&gt;&#10;&lt;li&gt;Hugo braucht &lt;code&gt;go&lt;/code&gt;. Klingt soweit logisch.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="runner-betriebssystem-anpassen"&gt;Runner-Betriebssystem anpassen&lt;/h3&gt;&#10;&lt;p&gt;Mein Runner aus dem Jahr 2023 ist inzwischen in die Tage gekommen. Er basiert auf &lt;code&gt;node:16-bullseye&lt;/code&gt;, eine Debian-basierte VM. Ich probiere etwas mit &lt;a href="https://hub.docker.com/_/node?tag=lts-alpine3.24" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Alpine&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; herum, doch diese Installation ist zu minimal für meine Zwecke und lässt mich mit vielen manuellen Anpassungen zurück. Schließlich muss der Runner noch andere Workflows ausführen als diesen hier. Ich entscheide mich letztendlich, bei Debian zu bleiben und nur die Node-Version hochzuziehen. Die Umsetzung in der Runner-Konfiguration sieht dann wie folgt aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /gitea/runner/config.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# The labels of a runner are used to determine which jobs the runner can run, and how to run them.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Like: [&amp;#34;macos-arm64:host&amp;#34;, &amp;#34;ubuntu-latest:docker://node:16-bullseye&amp;#34;, &amp;#34;ubuntu-22.04:docker://node:16-bullseye&amp;#34;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# If it&amp;#39;s empty when registering, it will ask for inputting labels.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# If it&amp;#39;s empty when execute `deamon`, will use labels in `.runner` file.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;labels&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;ubuntu-latest:docker://node:20-bullseye]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Anschließend muss der Runner-Container neu gestartet werden. Idealerweise bootet man Gitea gleich mit durch, dann funktioniert das Registrieren des Runners auch zuverlässig.&lt;/p&gt;&#10;&lt;h3 id="das-workflow-script-schreiben"&gt;Das Workflow-Script schreiben&lt;/h3&gt;&#10;&lt;p&gt;Um die oben beschriebenen Abhängigkeiten abzudecken entscheide ich mich für einen &amp;ldquo;batteries included&amp;rdquo;-Ansatz. So kann ich die Bausteine einfach ohne manuelle Anpassungen zusammenstecken und vertraue auf langfristige Wartung und Pflege der Container, da sie wirklich sehr häufig verwendet werden (Größenordnung mehrere Millionen Pulls). &lt;a href="https://docker.hugomods.com/docs/tags/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Diese Webseite&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; half mir sehr bei der Auswahl.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: Runner mit Alpine3.24 ist doch möglich.&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-09-22T00:00:00Z"&gt;&#10; 22. Sept. 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Führe ich die Action in einem Container aus wie unten beschrieben, so benötigt der Runner selbst gar kein NodeJS mehr, da der Checkout jetzt im Hugo Container selbst abläuft. Daher kann das OS des Runner wieder abgespeckt und &lt;code&gt;labels: [ubuntu-latest:docker://node:20-alpine3.24]&lt;/code&gt; verwendet werden, sofern keine andere Action einen direkten Checkout durchführen muss.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;p&gt;Schließlich erstelle ich meinen Workflow basierend auf dem Container von &lt;a href="https://hub.docker.com/r/hugomods/hugo" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hugomods&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /.gitea/workflows/build-deploy-with-hugo.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Deploy Hugo site&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;run-name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ gitea.actor }} builds Hugo site&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;on&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;push&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;branches&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;main&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;jobs&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Build job&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;build&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt; &lt;span style="color:#75715e"&gt;# this is the &amp;#34;label&amp;#34; the runner will use and map to docker target OS&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container&lt;/span&gt;: &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;hugomods/hugo:latest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;: &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/tmp/blog-artifacts:/tmp/blog-artifacts&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;CHECKOUT ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v4&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;submodules&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;recursive &lt;/span&gt; &lt;span style="color:#75715e"&gt;# Fetches Hugo themes&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;fetch-depth&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;BUILD WITH HUGO ---&lt;/span&gt; &lt;span style="color:#75715e"&gt;# chown -R hugo /tmp/blog-artifacts&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; hugo \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --minify \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --cleanDestinationDir \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --destination /tmp/blog-artifacts&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="zielordner-bereinigen"&gt;Zielordner bereinigen&lt;/h3&gt;&#10;&lt;p&gt;Ich habe mich im obigen Beispiel für ein &amp;ldquo;direktes Deployment&amp;rdquo; entschieden, ohne Netz und doppelten Boden, und ohne, dass ich die Artefakte herumkopiere oder gar den Server neu starte. Begründung: Der Hugo-Bauprozess benötigt für Meine Seite weniger als &lt;code&gt;2000ms&lt;/code&gt;. Nur in diesem Fenster kann Caddy theoretisch eine &amp;ldquo;korrupte&amp;rdquo; Datenbank sehen und ausliefern, also nur während die Platte vom Hugo-Build-Workflow noch beschrieben wird.&lt;/p&gt;&#10;&lt;p&gt;Um Inkonsistenzen trotzdem weitgehend zu vermeiden, gebe ich Hugo die Option &lt;code&gt;--cleanDestinationDir&lt;/code&gt; mit. Damit ist das Zielverzeichnis stets sauber und Caddy wird im Zweifel keine defekte Seite anzeigen, sondern höchstens eine &lt;code&gt;404-Fehlermeldung&lt;/code&gt; erzeugen.&lt;/p&gt;&#10;&lt;h3 id="submodules"&gt;Submodules&lt;/h3&gt;&#10;&lt;p&gt;Wie in &lt;a href="https://blog.schallbert.de/projects/migrating-jekyll-to-hugo/#opportunities"&gt;meiner Projektdokumentation&lt;/a&gt; geschrieben, kann man in Hugo zum Laden von Themes sogenannte &lt;em&gt;submodules&lt;/em&gt; verwenden. Diese werden in das Projekt eingeklickt und können, wenn korrekt verlinkt, dann auch im Workflow verwendet werden:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;submodules&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;recursive &lt;/span&gt; &lt;span style="color:#75715e"&gt;# Fetches Hugo themes&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;fetch-depth&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ist alles richtig eingestellt, sollte es nicht zu folgender Fehlermeldung kommen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# workflow action&amp;#39;s error message due to incoherent submodules&amp;#39; commit references&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# actions/checkout@v4:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Fetching submodules&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/git submodule sync&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/git -c protocol.version&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; submodule update --init --force&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Submodule &lt;span style="color:#e6db74"&gt;&amp;#39;themes/terminal&amp;#39;&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;https://github.com/panr/hugo-theme-terminal.git&lt;span style="color:#f92672"&gt;)&lt;/span&gt; registered &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; path &lt;span style="color:#e6db74"&gt;&amp;#39;themes/terminal&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Cloning into &lt;span style="color:#e6db74"&gt;&amp;#39;/workspace/schallbert/blog-hugo/themes/terminal&amp;#39;&lt;/span&gt;...&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fatal: remote error: upload-pack: not our ref 719505fc89332baa69bffb90cee708ff124dd143&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Fetched in submodule path &lt;span style="color:#e6db74"&gt;&amp;#39;themes/terminal&amp;#39;&lt;/span&gt;, but it did not contain 719505fc89332baa69bffb90cee708ff124dd143. Direct fetching of that commit failed.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Error: The process &lt;span style="color:#e6db74"&gt;&amp;#39;/usr/bin/git&amp;#39;&lt;/span&gt; failed with exit code &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Error: Process completed with exit code 1.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Voraussetzung ist, dass man die Submodule auch lokal richtig geladen hat:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~ git submodule add -f https://github.com/&amp;lt;my/hugo-theme&amp;gt;.git themes/my-hugo-theme&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~ git submodule update --init --recursive&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die Statusabfrage darf keinen leeren String zurückgeben, sondern muss einen Commit hash beinhalten. Beispiel:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~ git submodule status &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;44d9a1890d228745ffc300b37a7d73e940ef9fa9 themes/terminal &lt;span style="color:#f92672"&gt;(&lt;/span&gt;v4.2.5&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Weiteres Zeugnis ist die &lt;code&gt;.gitmodules&lt;/code&gt;-Datei, welche eine Referenz auf das Theme beinhalten muss:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:➜/blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;main&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ nano .gitmodules&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;submodule &lt;span style="color:#e6db74"&gt;&amp;#34;themes/terminal&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; path &lt;span style="color:#f92672"&gt;=&lt;/span&gt; themes/terminal&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; https://github.com/panr/hugo-theme-terminal.git&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Funktioniert das nicht, so sind die Submodule nochmal komplett zu entfernen und antschließend wie oben beschrieben neu zu initialisieren:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git submodule deinit -f themes/&amp;lt;my-theme&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git rm -r --cached themes/&amp;lt;my-theme&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rm -rf .git/modules/themes/&amp;lt;my-theme&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="typische-probleme-hugo-versionen-und-deprecations"&gt;Typische Probleme: Hugo-Versionen und Deprecations&lt;/h2&gt;&#10;&lt;p&gt;Nun verlief die &lt;code&gt;checkout&lt;/code&gt;-Aktion erfolgreich. Doch der Build mit Hugo schlägt fehl:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;error&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;calling&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;/workspace/schallbert/blog-hugo/layouts/_partials/head.html:37:40&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;execute&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;of&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;template&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;failed&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;template&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;_partials&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;head&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;html&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;37&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;executing&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;_partials/head.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;at&lt;/span&gt; &amp;lt;&lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Page&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Language&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Locale&lt;/span&gt;&amp;gt;: &lt;span style="color:#a6e22e"&gt;can&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;&amp;#39;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;t&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;evaluate&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;field&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Locale&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;in&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;langs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Language&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Error&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;Process&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;completed&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;with&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;exit&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;code&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Lokal auf meiner Maschine baut Hugo erfolgreich, gibt aber ein paar Warnungen heraus:&lt;/p&gt;&#10;&lt;h3 id="änderungen-der-language-identifier-und--auswertung-ab-v0158"&gt;Änderungen der Language-identifier und -Auswertung ab v0.158&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:➜/blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;main&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ hugo build --logLevel info &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WARN deprecated: site.Language.Locale was deprecated in Hugo v0.158.0 and will be removed in a future release. Use .Page.Language.Locale instead.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WARN deprecated: .Language.Lang was deprecated in Hugo v0.158.0 and will be removed in a future release.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Man könnte denken, dass sich diese Fehler leicht beheben ließen. Leider können sie sich prima verstecken:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;in Submodules, die man als third-party-content lädt (z.B. in Themes). Eine projektweite Suche in der IDE hat Submodules üblicherweise ausgeblendet und liefert keine Treffer.&lt;/li&gt;&#10;&lt;li&gt;als Variablen im eigenen Code, die nur indirekt auf die alten Parameter zugreifen: Ich benutze bis dato &lt;code&gt;.Page.Site.Home.AllTranslations&lt;/code&gt;, um herauszufinden, ob ich mich auf einer gegebenen Seite in der &amp;ldquo;Hauptsprache&amp;rdquo; aufhalte. Allein schon das Scoping ist widersinnig und wird meiner Meinung nach zu Recht entfernt. Das, was ich tun will, kann nun viel einfacher erreicht werden mit &lt;code&gt;.Page.Language.IsDefault&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="submodules-auf-stand-bringen"&gt;Submodules auf Stand bringen&lt;/h3&gt;&#10;&lt;p&gt;Bei den Submodulen sehe ich zwei Lösungsansätze:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Die Maintainer anschreiben (bzw. ein Issue aufmachen), dass ihr Code gerade veraltet und sie darum bitten, ihre Themes auf Stand zu bringen.&lt;/li&gt;&#10;&lt;li&gt;Die betroffenen Dateien aus dem Submodule herauskopieren und in die eigene Ordnerstruktur integrieren. So würde das in meinem Falle betroffene &lt;code&gt;language-menu.html&lt;/code&gt; unter &lt;code&gt;/layouts/partials/language-menu.html&lt;/code&gt; eine neue Heimat finden. Dort behebe ich dann die Probleme selbst.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="eigene-lösungen-programmieren"&gt;Eigene Lösungen programmieren&lt;/h3&gt;&#10;&lt;p&gt;Macht die Fehlermeldung keine Lösung offensichtlich, so kann man sich gut an der Hugo Community orientieren. Für die &lt;a href="https://discourse.gohugo.io/t/deprecations-in-v0-158-0/56869" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Deprecatoins in v0.158&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gibt es beispielsweise eine ausführliche Dokumentation.&lt;/p&gt;&#10;&lt;h3 id="versionen-abgleichen"&gt;Versionen abgleichen&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-09-15-hugo-build-v0154-fail.avif" alt="Image: Gitea&amp;#39;s workflow run log in detail view. The build fails with an error about a field that cannot be evaluated."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Hmm. Wie das Bild zeigt, verwendet mein Container das Paket &lt;code&gt;hugo extended&lt;/code&gt; von hugomods, welches die Version &lt;code&gt;v0.154.5&lt;/code&gt; bereitstellt; ich hatte einen stabilen release für die Action ausgewählt. Vergleichen wir mal mit meiner lokalen Version, die inzwischen ohne auch nur eine Warnung brav baut:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;main&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ hugo version&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;hugo v0.162.1+extended linux/amd64 &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Der Klassiker: Es gibt eine Änderung ab &lt;code&gt;v0.158&lt;/code&gt;, die die interne API verändert. Ältere Versionen kennen die neuen Felder nicht und meckern. Neuere Versionen hingegen kreiden die Verwendung der alten Felder an.&lt;/p&gt;&#10;&lt;p&gt;Ich glaube, diese Probleme lassen sich nur dann zuverlässig vermeiden, wenn die Pipeline für lokal und remote identisch ist. Sprich: Auf dem Lokalen Rechner eine virtuelle Maschine mit dem CI-Betriebssystem booten und alles in Docker wegkapseln. Macht bestimmt Sinn, wenn man in einer größeren Organisation arbeitet und mehrere User vor diesem Problem stehen.&lt;/p&gt;&#10;&lt;p&gt;Stattdessen wechsle ich zur Behebung im Remote Container auf die Version &lt;code&gt;image: hugomods/hugo:latest&lt;/code&gt; und behebe damit den Fehler. Jedoch habe ich noch immer unterschiedliche Versionen lokal und in der CD-Pipeline. Für mein Betriebssystem gibt es die neueste schlicht noch nicht und auch der Docker Release hängt zeitlich etwas hinter den Releases des Originals.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-09-30-CD-build-with-hugo.avif" alt="Image: Gitea dashboard in the Actions tab. We see three successful workflow runs with a hugo full build &amp;amp; deploy of my site, taking 12sec, 9sec, and 21sec."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Die Bau- und Releasezeiten sind fantastisch, oder? Eine Änderung an der Website in &lt;code&gt;12 Sekunden&lt;/code&gt; online zu haben, inkl. Container hochziehen etc. - das gab es bei mir jedenfalls noch nie. Um die &lt;code&gt;20 Sekunden&lt;/code&gt; benötigt der Durchlauf, wenn er die Container refs erst neu holen und herunterladen muss, z.B. bei Updates der Registry von &lt;code&gt;latest&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Einen detaillierten &lt;a href="https://blog.schallbert.de/hugo-versus-jekyll-benchmark/"&gt;Benchmarkd zwischen Hugo und Jekyll&lt;/a&gt; bietet der verlinkte Artikel.&lt;/p&gt;&#10;</description></item><item><title>Schallberts Blog mit Hugo</title><link>https://blog.schallbert.de/announcements/2026-09-06-blog-anniversary-hugo/</link><pubDate>Sun, 06 Sep 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/announcements/2026-09-06-blog-anniversary-hugo/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/migrating-hugo-to-jekyll/hugo-website-landing-cover.avif"&#10; class="post-cover"&#10; alt="Image: Crop my landing page with Hugo: We see the logo of the blog, half a speaker and half cog-wheel as menu bar, simple three-word navigation, and the cover images representing the blog&amp;#39;s focus points on Hardware, Electronics, and Software."&#10; title="Schallberts Blog mit Hugo" /&gt;&#10;&lt;h2 id="was-ist-geschehen"&gt;Was ist geschehen?&lt;/h2&gt;&#10;&lt;p&gt;Zum fünften Geburtstag 🥳 stelle ich &lt;strong&gt;schallbert&amp;rsquo;s Blog&lt;/strong&gt; auf eine neue technische Basis!&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/migrating-hugo-to-jekyll/hugo-jekyll-build-compare-banner.avif" alt="Image: side-by-side comparison of my website, once built with jekyll, and now built with hugo."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Ich habe diesen Blog von &lt;a href="https://blog.schallbert.de/projects/thissite/"&gt;jekyll&lt;/a&gt; nun auf den Webseitengenerator &lt;a href="https://gohugo.io/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hugo&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; umgezogen. Das war viel Aufwand: Über etwa vier Monate hinweg habe ich einen Großteil meiner Freizeit hineingesteckt. Dabei ging der Löwenanteil in das Erstellen und Polieren der Formatierung, Anpassung meiner Inhalte an die neue Umgebung, Reparatur defekter Links und die Dokumentation meines Umbaus.&lt;/p&gt;&#10;&lt;h2 id="warum-habe-ich-das-gemacht"&gt;Warum habe ich das gemacht?&lt;/h2&gt;&#10;&lt;p&gt;Mit Jekyll stand ich immer mehr auf Kriegsfuß: Viele Abhängigkeiten mit immer häufigeren Defekten, die zunehmend lange Bauzeit und die komplexe Umgebung zum Ausrollen auf meinem Server waren die Hauptgründe.&lt;/p&gt;&#10;&lt;p&gt;Meine Seite baut nun etwa &lt;em&gt;fünf mal so schnell&lt;/em&gt; wie vorher.&lt;/p&gt;&#10;&lt;p&gt;Sie lädt &lt;em&gt;weniger Formatierung&lt;/em&gt; nach (CSS, JS) und die Bilder sind noch einmal &lt;em&gt;platzsparender&lt;/em&gt;.&lt;/p&gt;&#10;&lt;p&gt;Außerdem habe ich weder Ruby noch zig Gems auf Stand zu halten.&lt;/p&gt;&#10;&lt;p&gt;Bei den Themes habe ich in Hugo Wert darauf gelegt, es nach benötigter Funktion auszuwählen und nicht nur nach gefälligem Aussehen. Somit ist es jetzt viel &lt;em&gt;simpler und performanter&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h2 id="was-haben-die-leserinnen-davon"&gt;Was haben die LeserInnen davon?&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/announcements/2026-09-06-rss.webp" alt="Image: the RSS logo showing an orange frame with white lines and a dot, simplified like waves transmitted by a broadcast antenna."&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;RSS logo&lt;/span&gt;&lt;a&#10; href="https://rss.com/blog/free-rss-icon/#rss-logo"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;Der Blog bietet nun einen &lt;a href="https://blog.schallbert.de/index.xml/"&gt;RSS-Feed&lt;/a&gt;. Außerdem sind nun auch meine Projekte in die &lt;a href="https://blog.schallbert.de/tags/"&gt;Schlagwort-Ansicht&lt;/a&gt; mit eingebunden. Zu guter Letzt sind durch die Migration zahlreiche Artikel entstanden. Diese sollen einen solchen Umzug für andere leichter machen als ich es hatte, und die Strukturen in Hugo verdeutlichen.&lt;/p&gt;&#10;&lt;h2 id="artikel-zum-thema"&gt;Artikel zum Thema&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/projects/migrating-jekyll-to-hugo/"&gt;Projekt: Migration von Jekyll zu Hugo&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/hugo-create-language-specific-permalinks/"&gt;Permalinks sprachunabhängig korrekt darstellen&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/hugo-link-checker/"&gt;Website-Links in Hugo mit &lt;em&gt;linkcheck&lt;/em&gt; überprüfen&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/migrate-jekyll-to-hugo-image-alignment/"&gt;Bildformatierung und -Platzierung in Hugo&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/build-deploy-hugo-with-actions-docker-caddy/"&gt;Continuous Deployment: Pipeline für Hugo bauen&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="was-fehlt-noch"&gt;Was fehlt noch?&lt;/h2&gt;&#10;&lt;p&gt;Ich bin mit dem Umbau noch nicht ganz fertig:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Die Website auch für die mobile Darstellung optimieren ✓&lt;/li&gt;&#10;&lt;li&gt;Es soll möglich sein, Bilder zentriert und in nativer Auflösung anzuzeigen ✓&lt;/li&gt;&#10;&lt;li&gt;Der Feed soll Coverbilder meiner Artikel korrekt anzeigen ✓&lt;/li&gt;&#10;&lt;li&gt;Ich möchte in CSS einen noch einheitlicheren Look haben (Farben, Bildränder etc.)&lt;/li&gt;&#10;&lt;li&gt;Übersichtsseiten könnten die ganze Bildschirmseite einnehmen und dann größere Bilder anzeigen&lt;/li&gt;&#10;&lt;li&gt;Es fehlen einige englische Übersetzungen ✓&lt;/li&gt;&#10;&lt;li&gt;Schlagworte könnten spezifischer und trennschärfer implementiert werden&lt;/li&gt;&#10;&lt;li&gt;Schön wäre ein mitscrollendes Inhaltsverzeichnis auf der linken Seite&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Auf diesen Seiten wird es in den kommende Wochen in vielen Details noch einmal Verbesserungen geben.&#10;Ich freue mich darauf!&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;schallbert&lt;/strong&gt;&lt;/p&gt;&#10;</description></item><item><title>Umzug von Jekyll zu Hugo</title><link>https://blog.schallbert.de/projects/migrating-jekyll-to-hugo/</link><pubDate>Sun, 06 Sep 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/migrating-jekyll-to-hugo/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/migrating-hugo-to-jekyll/hugo-jekyll-build-compare-cover.avif"&#10; class="post-cover"&#10; alt="Image: Hugo console build output. It shows a full build time below 8sec. (~70% reduction over build time with Jekyll)"&#10; title="Umzug von Jekyll zu Hugo" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeitsgrad: Mittel 3/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: 0€&lt;/li&gt;&#10;&lt;li&gt;Zeit ~20h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;In diesem Projekt beschreibe ich meinen Umzug von Jekyll mit der Optik von &lt;a href="https://github.com/mmistakes/minimal-mistakes" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Minimal Mistakes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zu Hugo in einem stark modifizierten &lt;a href="https://github.com/panr/hugo-theme-terminal" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Terminal-Theme&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="warum-überhaupt-wechseln"&gt;Warum überhaupt wechseln?&lt;/h2&gt;&#10;&lt;p&gt;Schon seit langem war ich nicht mehr vollständig zufrieden mit dem Bauen meiner Website per Jekyll. Der Auslöser für den Umstieg war, dass ich eines Tages plötzlich wegen eines Kompatibilitätsproblemes nicht mehr lokal bauen konnte. Ein &lt;code&gt;bundle update --conservative&lt;/code&gt; oder &lt;code&gt;bundle install&lt;/code&gt; wie üblich half nicht. Auch das manuelle Nachinstallieren der verursachenden Pakete per &lt;code&gt;gem install commonmarker-0.23.12&lt;/code&gt; oder &lt;code&gt;gem install posix-spawn -v 0.3.15 -- --with-cflags=\&amp;quot;-Wno-incompatible-function-pointer-types\&amp;quot;&lt;/code&gt; brachten keine Abhilfe.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/migrating-hugo-to-jekyll/jekyll-update-bundle-installer-error.avif" alt="Image: Bundler dependency tree showing the error: Failed to build gem native extension. Multiple errors like this occurred lately when I tried building my site."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Ein Update von &lt;em&gt;Ruby&lt;/em&gt; und &lt;em&gt;Jekyll&lt;/em&gt; selbst brachte ebenfalls keinen Erfolg. Und überhaupt: Die Bauzeit mit Jekyll ist inzwischen ziemlich lang geworden. Ich bin durch meinen &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/"&gt;Umzug der Seite von Github Pages auf einen eigenen Server&lt;/a&gt; nicht mehr an Jekyll gebunden, was einen Wechsel attraktiver macht.&lt;/p&gt;&#10;&lt;h3 id="weitere-schwierigkeiten-mit-jekyll"&gt;Weitere Schwierigkeiten mit Jekyll&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;CI&amp;amp;CD: Meine Deploy-Pipeline war vor Monaten mit ähnlichen Fehlern ausgefallen, sodass ich auf eine ältere Jekyll-Version zurückgreifen musste. Damit vergrößert sich technische Schuld und Angriffsfläche meiner Actions. Ich müsste auf die Suche nach einer gut gewarteten Containerlösung eines anderen Anbieters wechseln. Suche und Einrichtung würden zusätzliche Zeit erfordern.&lt;/li&gt;&#10;&lt;li&gt;RSS-Feed: Ich möchte zu meiner Website gern einen RSS-Feed bereitstellen. Meine Experimente mit Jekyll zeigten jedoch, dass nur &amp;ldquo;Posts&amp;rdquo; und keine &amp;ldquo;Announcements&amp;rdquo; inkludiert werden. Das wollte ich beheben.&lt;/li&gt;&#10;&lt;li&gt;Bildformate: Cover-Bilder (Thumbnails) für Posts werden im RSS-Feed durch Jekyll eigentlich mit gebaut. Durch meinen Umstieg von &lt;code&gt;.jpg&lt;/code&gt; auf das platzsparendere &lt;code&gt;.avif&lt;/code&gt; funktionierte das nun leider nicht mehr. Eine Lösung habe ich trotz mehrerer Anläufe nicht durch den Pull Request Review bekommen.&lt;/li&gt;&#10;&lt;li&gt;Icons: Das von mir verwendete Theme nutzt &lt;em&gt;Font Awesome&lt;/em&gt; zur Darstellung von Icons und ist insgesamt für eine statische Website recht groß. Ich wollte aber nichts aus dem Netz nachladen und habe die Bibliothek daher lokal gespeichert. Daher war mein Plan, nur die wirklich benötigten Icons heraussuchen und den Rest zu löschen. Mit dem Umstieg auf Hugo ist dies nicht mehr notwendig.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="opportunities"&gt;Die Chancen&lt;/h3&gt;&#10;&lt;p&gt;Von Hugo verspreche ich mir schnellere Bauzeiten, eine vereinfachte CI/CD ohne Nachinstallieren von Bundler, Ruby etc. und die Behebung der oben beschriebenen Schwierigkeiten. Das Abhängigkeits- und Paketmanagement von Hugo unterscheidet sich grundlegend von Jekyll: Zusätzliche Inhalte werden per &lt;a href="https://git-scm.com/book/en/v2/Git-Tools-Submodules" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Git Submodules&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; eingebunden und das Abhängigkeismanagement nutzt statt der &lt;a href="https://bundler.io/man/gemfile.5.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gemfile&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; schlicht &lt;a href="https://instagit.com/gohugoio/hugo/hugo-dependency-management-go-modules/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Go modules&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="das-migrationswerkzeug"&gt;Das Migrationswerkzeug&lt;/h2&gt;&#10;&lt;p&gt;Die Ordnerstrukturen der Inhalte unterscheidet sich deutlich zwischen &lt;em&gt;Jekyll&lt;/em&gt; und &lt;em&gt;Hugo&lt;/em&gt;. Ich nutze daher ein in Hugo enthaltenes &lt;a href="https://gohugo.io/commands/hugo_import_jekyll/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Importwerkzeug&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, welches mir zumindest die Artikel unter &lt;code&gt;/posts&lt;/code&gt; korrekt importiert.&lt;/p&gt;&#10;&lt;p&gt;Alle spezifischen Inhalte wie Übersichtsseiten &lt;code&gt;/tags, /pages/error, /pages/legal&lt;/code&gt; und Seiten, wo Projekte und Ankündigungen abgelegt sind &lt;code&gt;/_announcements, /_projects&lt;/code&gt;, werden allerdings nicht mit umgezogen und müssen manuell kopiert und modifiziert werden.&lt;/p&gt;&#10;&lt;h3 id="anpassen-der-front-matter"&gt;Anpassen der Front Matter&lt;/h3&gt;&#10;&lt;p&gt;Viele von mir gemachten Anpassungen in Jekyll funktionieren in Hugo entweder nicht richtig oder führen zum Abbruch des Bauvorganges - die &lt;strong&gt;Front Matter&lt;/strong&gt; einer jeden Markdown-Datei muss also angepasst und teils bereinigt werden.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Alt: So kann die Front matter einer Übersichtsseite in Jekyll aussehen&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;lang&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;de&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Beitragsarchiv&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;subtitle&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Diese Sammlung enthält alle meine Beiträge&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;layout&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;collection&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;collection&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;posts&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;permalink&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/posts-archive/&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;entries_layout&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;grid&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;classes&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;wide&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;author_profile&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In meinem Falle liegen einige Theme-abhänigen Parameter im Frontmatter vor wie &lt;code&gt;layout, classes, author_profile&lt;/code&gt;. Der &lt;code&gt;lang&lt;/code&gt;-Parameter aus meiner Internationalisierungserweiterung in Jekyll ist nicht einmal korrekte Hugo-Syntax: es erwartet die angezeigte Sprache entweder als Feld unter &lt;code&gt;params: lang&lt;/code&gt; oder Ordnerweit. Auch andere, an den eigenen Geschmack angepasste Parameter können definiert und unter &lt;code&gt;params&lt;/code&gt; mitgegeben werden.&lt;/p&gt;&#10;&lt;p&gt;Permalinks finden gar keinen Eingang mehr in die Front matter, stattdessen findet die Konfiguration &lt;a href="https://gohugo.io/configuration/permalinks/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;zentral statt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Eine &lt;a href="https://gohugo.io/content-management/front-matter/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hugo-kompatible front matter&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kann so aussehen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Beitragsarchiv&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;description&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Diese Sammlung enthält alle meine Beiträge&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;layout&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;collection&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="konfiguration-des-theme"&gt;Konfiguration des Theme&lt;/h2&gt;&#10;&lt;p&gt;Eine Basis-Konfiguration meines Themes kann ich mit folgendem Kommando installieren:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;main&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git submodule add https://github.com/panr/hugo-theme-terminal ./themes/terminal &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Zum Aktivieren muss ich es in der Hauptkonfiguration bekannt machen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /hugo.toml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Add it only if you keep the theme in the `themes` directory.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Remove it if you use the theme as a remote Hugo Module.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;theme&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;terminal&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und schon baut Hugo für mich im Terminal-Theme und Originalfarben.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/migrating-hugo-to-jekyll/hugo-initial-build-with-terminal.avif" alt="Image: One of my blog posts in original Terminal theme: Left-adjusted, filling half the screen and in dark grey, orange as contrast color, and white text."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Dies ist nun nicht vertikal zentriert und für meinen Geschmack zu schmal ausgelegt, der Text recht klein und &amp;ldquo;meine&amp;rdquo; Blog-Farben sollen ebenfalls anders sein .&lt;/p&gt;&#10;&lt;p&gt;Daher erstelle ich eine &lt;code&gt;.css&lt;/code&gt;-Datei, die einige Eigenschaften des Basis-Themes überschreibt. Sie muss im Ordner &lt;code&gt;/static&lt;/code&gt; liegen und den Namen des Themes tragen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/* /static/terminal.css */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;:&lt;span style="color:#a6e22e"&gt;root&lt;/span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --background: &lt;span style="color:#ae81ff"&gt;#252a34&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --background-highlight: &lt;span style="color:#ae81ff"&gt;#475064&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --foreground: &lt;span style="color:#ae81ff"&gt;#d2eaef&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --foreground-dimmed: &lt;span style="color:#ae81ff"&gt;#8b9ea2&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --accent: &lt;span style="color:#ae81ff"&gt;#25a679&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --accent-highlight: &lt;span style="color:#ae81ff"&gt;#79ae9c&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --radius: &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --font-size: &lt;span style="color:#ae81ff"&gt;1.35&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --line-height: &lt;span style="color:#ae81ff"&gt;1.8&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;em&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/* Use system fonts, reduce transferred kB */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;body&lt;/span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;font-family&lt;/span&gt;: system-ui, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;apple-system, &lt;span style="color:#e6db74"&gt;&amp;#34;Segoe UI&amp;#34;&lt;/span&gt;, Roboto, Arial, &lt;span style="color:#66d9ef"&gt;sans-serif&lt;/span&gt; &lt;span style="color:#75715e"&gt;!important&lt;/span&gt;; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/* make content wider */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;body&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;container&lt;/span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;70&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;90&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;%&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und schon sieht es fast wie früher aus.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/migrating-hugo-to-jekyll/hugo-custom-build-with-terminal.avif" alt="Image: One of my blog posts in the modified Terminal theme: centered, filling three quarters of the screen and in dark grey, teal as contrast color, and blueish-white text with a sans-serif system font."&gt;&lt;/figure&gt;&#10;&lt;h2 id="organisation-der-ordnerstruktur"&gt;Organisation der Ordnerstruktur&lt;/h2&gt;&#10;&lt;p&gt;Anders als bei der von mir verwendeten &lt;em&gt;Jekyll&lt;/em&gt;-Version verfügt &lt;em&gt;hugo&lt;/em&gt; über eingebaute Internationalisierung (&lt;code&gt;i18n&lt;/code&gt;). Dafür muss ich meine Ordnerstruktur umbauen und explizit in &lt;code&gt;/de&lt;/code&gt; und &lt;code&gt;/en&lt;/code&gt;-Ordnerinhalte sortieren. Inhalte werden nun statt auf der höheren Ordner-Ebene direkt unter &lt;code&gt;/_pages, /_announcements, /_posts&lt;/code&gt; im Ordner &lt;code&gt;content&lt;/code&gt; gehalten, was die Sache übersichtlicher macht.&lt;/p&gt;&#10;&lt;h3 id="vorher"&gt;Vorher&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _announcements&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── assets&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── css&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── fontawesome&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── katex&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── main.scss &lt;span style="color:#75715e"&gt;# &amp;lt;-- theme import, customization&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ └── webfonts&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── js&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── lunr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── _main.js&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── main.min.js&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── plugins&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ └── vendor&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── video&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── banner.js&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── CNAME&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _config.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _data&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── de&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── l10n.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── en&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── l10n.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── en&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── index.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── Gemfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── Gemfile.lock&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _includes &lt;span style="color:#75715e"&gt;# &amp;lt;-- Layouts (HTML), partials, 3rd-party&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── index.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- Landing page&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _layouts &lt;span style="color:#75715e"&gt;# &amp;lt;-- Page layouts&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _pages&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── 404.md &lt;span style="color:#75715e"&gt;# &amp;lt;-- German version&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── &lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── en&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── 404.md &lt;span style="color:#75715e"&gt;# &amp;lt;-- English version&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── &lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _posts/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── en &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _projects/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _sass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── minimal-mistakes &lt;span style="color:#75715e"&gt;# &amp;lt;-- Styles (SCSS) for layouts and partials&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── minimal-mistakes.scss&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── _video.scss&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _site/ &lt;span style="color:#75715e"&gt;# &amp;lt;-- Build artifacts&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── staticman.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="nachher"&gt;Nachher&lt;/h3&gt;&#10;&lt;p&gt;Einen guten Überblick über die Ordnerstruktur bietet &lt;a href="https://gohugo.io/templates/new-templatesystem-overview/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hugos Dokumentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Am wichtigsten für die tägliche Arbeit mit Artikeln und Aussehen der Website sind die Stammordner &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;layouts&lt;/code&gt; und &lt;code&gt;static&lt;/code&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── archetypes&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── assets&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── content&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── de&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── about.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── announcements/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── posts/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ └── projects/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── en&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── about.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── &lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── data&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── hugo.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── layouts&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── _default&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── baseof.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- defines header, add-ons, content, footer structure&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── index.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- landing page&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── list.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- grid view&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ └── single.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- post view&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── _markup&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ └── render-link.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- link highlighting and function (referrer, tabs)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── _partials&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── cover.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- cover (thumbnail) image rendering&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ └── math.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- math rendering (katex)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── _shortcodes&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── audio.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── image.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── &lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── resources&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── _gen&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── assets/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── static&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── assets&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── audio/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── css/ &lt;span style="color:#75715e"&gt;# &amp;lt;-- contains overrides for custom layouts &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── docs/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── images/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── js/ &lt;span style="color:#75715e"&gt;# &amp;lt;-- math (katex) JS lives here&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ └── video/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── terminal.css &lt;span style="color:#75715e"&gt;# &amp;lt;-- Central theme override (colors, formatting, styling)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── themes&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── terminal &lt;span style="color:#75715e"&gt;# &amp;lt;-- GIT submodule: Vanilla &amp;#34;Terminal&amp;#34; theme&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bei beiden Lösungen werden Layouts in &lt;code&gt;html&lt;/code&gt; und Stil/Format ind &lt;code&gt;CSS&lt;/code&gt; definiert. Da ich bei Jekyll mehr Plugins verwenden musste, wird das Setup größer.&lt;/p&gt;&#10;&lt;h2 id="manuelle-anpassungen-shortcodes"&gt;Manuelle Anpassungen: Shortcodes&lt;/h2&gt;&#10;&lt;p&gt;Mit &lt;em&gt;Shortcodes&lt;/em&gt; kann man in Hugo nicht-Text-Inhalte (Medien, Links, Formatierung, Stil) definieren, die dann nach definierter Vorschrift gerendert werden. Dafür legt man eine &lt;code&gt;html&lt;/code&gt;-Datei an, welche den Dekorator definiert und entsprechende &lt;code&gt;css-Klassen&lt;/code&gt; einbindet. Außerdem fügt man die gewünschte Formatierung in der überschreibenden Theme-&lt;code&gt;css&lt;/code&gt;-Datei ein.&lt;/p&gt;&#10;&lt;p&gt;Da ich hier absolut kein Profi bin, probiere ich anschließend so lange herum, bis mir das Layouting gefällt und ich keine Artefakte/Überlagerungen mehr sehe.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── layouts&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── _shortcodes&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── my-shortcode.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- Custom shortcode file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── static&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── terminal.css &lt;span style="color:#75715e"&gt;# &amp;lt;-- Central theme override (colors, formatting, styling)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="syntax-hugo-und-jekyll"&gt;Syntax: Hugo und Jekyll&lt;/h3&gt;&#10;&lt;p&gt;Shortcodes werden bei Hugo in der Markdown-Datei mit folgender Syntax angezeigt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&amp;lt; &lt;span style="color:#a6e22e"&gt;shortcode&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;html&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;filename&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;options&lt;/span&gt; &amp;gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&amp;lt; &lt;span style="color:#a6e22e"&gt;image&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;/path/to/image.avif&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;alt&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;Image: alt text&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;position&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;right&amp;#34;&lt;/span&gt; &amp;gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Im Vergleich dazu sehen Shortcode-ähnliche Dekoratoren in Jekyll für &lt;a href="https://jekyllrb.com/docs/liquid/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Liquid, die in Jekyll benutzte Beschreibungssprache&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; so aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-md" data-lang="md"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### Liquid in interpreter code&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{% Liquid bracket syntax %}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### Liquid &amp;#34;shortcode&amp;#34; pendant&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{% include gallery id=&amp;#34;gallery&amp;#34; caption=&amp;#34;Eindrücke von &lt;span style="font-weight:bold"&gt;**MobFobAmp**&lt;/span&gt;. Zum Vergrößern anklicken.&amp;#34; %}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### Liquid decorator for layouting&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{:.list-inline}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="image"&gt;image&lt;/h3&gt;&#10;&lt;p&gt;Anwendungsbeispiel zur Bildanzeige. In der nächsten Zeile folgt ein zentriertes Bild.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/favicon/logo_icon.avif" alt="Image: My blog logo, half a speaker chassis, half cog wheels, centered."&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/favicon/logo_icon.avif" alt="Image: My blog logo, half a speaker chassis, half cog wheels, left-adjusted with text flow."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Der klassische Bild-Dekorator für Markdown &lt;code&gt;[Alt Text](/path/to/image)&lt;/code&gt; wird in Hugo standardmäßig als mittig zentriertes Bild ohne Textfluss gerendert. Für ein schönes Layouting benötige ich jedoch ein paar Varianten, welche etwas zusätzlichen Code in cascading style sheets und html erfordern.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/favicon/logo_icon.avif" alt="Image: My blog logo, half a speaker chassis, half cog wheels, right-adjusted with text flow."&gt;&lt;/figure&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Bild zentriert, ohne Textfluss&lt;/li&gt;&#10;&lt;li&gt;Bild links , Textfluss rechts&lt;/li&gt;&#10;&lt;li&gt;Bild rechts, Textfluss links&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Zuerst besorgt sich Hugo Dateipfad und Bildtext. Anschließend liest es die gewünschte Formatierung aus der Markdown-Datei, welche mit der &lt;code&gt;position&lt;/code&gt;-Variable angelegt wird. Anschließend wendet sie die &lt;code&gt;image-wrapper&lt;/code&gt; Klasse aus der &lt;code&gt;.css&lt;/code&gt;-Datei darauf an. Die Ausrichtung der Bilder gilt für bessere Lesbarkeit nur für die Desktop-Variante der Webseite (&lt;code&gt;&amp;gt;684px&lt;/code&gt;).&lt;/p&gt;&#10;&lt;p&gt;Die Anpassungen des Themes gehen zur richtigen Darstellung von Bildern recht weit. Außerdem habe ich noch eine Galerie entwickelt, falls ein Post über sehr viele Bilder verfügt. Daher habe ich für die Bildformatierung und -Ausrichtung &lt;a href="https://blog.schallbert.de/migrate-jekyll-to-hugo-image-alignment/"&gt;einen eigenen Artikel&lt;/a&gt; geschrieben.&lt;/p&gt;&#10;&lt;h3 id="audio"&gt;audio&lt;/h3&gt;&#10;&lt;p&gt;Zur Darstellung des Audio-Players verwende ich die im Browser implementierten Standards.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--audio"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;audio controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/audio/aa_alpha_fingered_flageolet.mp3" type="audio/mp3"&gt;&#10; Your browser does not support the audio element.&#10; &lt;/audio&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Beispielplayer: Melodie auf dem E-Bass&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#75715e"&gt;/* /layouts/_shortcodes/audio.html */&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{ &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;src&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;src&amp;#34;&lt;/span&gt; }}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{ &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;caption&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;caption&amp;#34;&lt;/span&gt; | &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt; }}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;media-wrapper&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;media-container&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;audio&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;controls&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;source&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;{{ $src }}&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;type&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;audio/mp3&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Your&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;browser&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;does&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;support&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;the&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;audio&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;element&lt;/span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;audio&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{ &lt;span style="color:#a6e22e"&gt;with&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;caption&lt;/span&gt; }}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;media-title&amp;#34;&lt;/span&gt;&amp;gt;{{ . }}&amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{ &lt;span style="color:#a6e22e"&gt;end&lt;/span&gt; }}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In &lt;code&gt;css&lt;/code&gt; verwende ich einen Wrapper und Media-Container, um Titel im Rahmen des Players anzeigen zu können und die Formatierung zu erleichtern. Auch hier habe ich angepasstes Verhalten für ein &amp;ldquo;responsive layout&amp;rdquo; bei Mobilgeräten oder Änderungen der Größe des Browser-Fensters.&lt;/p&gt;&#10;&lt;h3 id="video"&gt;video&lt;/h3&gt;&#10;&lt;p&gt;Das CSS ist identisch mit &lt;code&gt;audio&lt;/code&gt;. Es gibt nur im &lt;code&gt;html&lt;/code&gt; geringfügige Unterschiede. Der von mir verwendete Container unterstützt unter &lt;code&gt;src=&lt;/code&gt; als Quellen sowohl &amp;ldquo;embed-Links&amp;rdquo; von Webseiten wie &lt;em&gt;peertube&lt;/em&gt; als auch lokale Quelldateien.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-03-01_trolley.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Video-Beispiel (lokal): Linearführungen mit Kugelumlauf&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#75715e"&gt;/* /layouts/_shortcodes/video.html */&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;src&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;src&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;title&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;title&amp;#34;&lt;/span&gt; | &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;caption&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;caption&amp;#34;&lt;/span&gt; | &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;media-wrapper&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;media-container&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;hasPrefix&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;src&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;http&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;iframe&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;{{ $src }}&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;frameborder&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;0&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;allowfullscreen&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;iframe&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;video&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;controls&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;source&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;{{ $src }}&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;type&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;video/mp4&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Your&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;browser&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;does&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;support&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;the&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;video&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;tag&lt;/span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;video&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;end&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;or&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;title&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;caption&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;media-title&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;title&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}{{ &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;title&lt;/span&gt; }}{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}{{ &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;caption&lt;/span&gt; }}{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;end&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;end&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="updates"&gt;Updates&lt;/h3&gt;&#10;&lt;p&gt;Auch bei mir &amp;ldquo;altern&amp;rdquo; Inhalte. Manchmal wechsle ich Technologien oder Anbieter, und manchmal bewegen sich auch Dritte von Lösungen weg, deren Implementierung ich hier im Blog beschrieben habe. Daher habe ich &lt;code&gt;update-shortcodes&lt;/code&gt; eingeführt.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Software altert&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-09-15T00:00:00Z"&gt;&#10; 15. Sept. 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Klingt komisch. Was an Software soll denn altern? Vielleicht lässt es sich besser formulieren, indem man sagt, dass sich die Zeit und damit die Welt über Software hinwegbewegt und man sie daher stetig anpassen muss. Erscheint ein solches Kästchen im Blog, gibt es neue Informationen zum Thema, die ergänzend oder ersetzend wirken können.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="verlinkungen"&gt;Verlinkungen&lt;/h2&gt;&#10;&lt;p&gt;Folgendes Verhalten wünsche ich mir für Links auf meinem Blog:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;interne Links sollen im selben Tab geöffnet werden und die Nutzer weiterleiten.&lt;/li&gt;&#10;&lt;li&gt;interne Links sollen zu Seiten in derselben Sprache verweisen und nicht auf die Standard-Sprache oder zurück zur Hauptseite führen.&lt;/li&gt;&#10;&lt;li&gt;externe Links sollen mit einem Symbol gekennzeichnet werden: &lt;code&gt;↗&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;externe Links sollen in einem frischen Tab geöffnet werden: &lt;code&gt;target=&amp;quot;_blank&amp;quot;&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;exterme Links sollen weder auf die ursprünglichen Inhalte zugreifen noch den Quellverweis sehen können &lt;code&gt;rel=&amp;quot;noopener noreferrer&amp;quot;&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Dafür erstelle ich unter &lt;code&gt;/layouts/_markup&lt;/code&gt; die Datei &lt;code&gt;render-link.html&lt;/code&gt;.&#10;Den Code hierfür und seine Hintergründe stelle ich in einem separaten Post vor: &lt;a href="https://blog.schallbert.de/hugo-create-language-specific-permalinks/"&gt;Hugo Multi-language: Interne Permalinks&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Der Override für CSS sieht wie folgt aus.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/* /static/terminal.css */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;external-link&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;external-link-icon&lt;/span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;inline-block&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;font-size&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;font&lt;span style="color:#f92672"&gt;-&lt;/span&gt;size);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;vertical-align&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;middle&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;transform&lt;/span&gt;: translateY(&lt;span style="color:#ae81ff"&gt;-0.1&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;em&lt;/span&gt;);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="manuelle-anpassungen-partials"&gt;Manuelle Anpassungen: Partials&lt;/h2&gt;&#10;&lt;p&gt;Partials kann man sich vorstellen wie die Layout-Bausteine für eine Hugo-Website. Sie werden unter &lt;code&gt;/layouts/_partials&lt;/code&gt; abgelegt und überschreiben die Standards des verwendeten Themes. Dabei wird die Ursprungsdatei gar nicht mehr geladen, was ein Herüberkopieren der Originaldatei in den Ordner mit anschließender manueller Anpassung empfehlenswert macht.&lt;/p&gt;&#10;&lt;h3 id="index"&gt;index&lt;/h3&gt;&#10;&lt;p&gt;Die Startseite für den Blog ist komplett selbt geschrieben und liegt nicht in Markdown, sondern in &lt;code&gt;html&lt;/code&gt; vor. Sie zeigt ihre Inhalte anders an als alle anderen Blog-Seiten und daher lohnte es sich für mich nicht, Templates zu programmieren. In der entsprechenden &lt;code&gt;/layouts/_partials/index.html&lt;/code&gt; lade ich dafür notwendige &lt;code&gt;scss&lt;/code&gt; und &lt;code&gt;css&lt;/code&gt;-Dateien und gebe die Struktur der Seite vor. Die folgende Tabelle gibt einen Überblick über meine Änderungen.&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;File&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Purpose&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Customization&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;cover&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;thumbnail / cover image for overview pages, lists, and grids&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Add custom cover image decorator, alt text&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;head&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Defines Favicon, search console, page parameters, feeds etc.&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Custom favicon path&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;header&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Page header containing menu, logo, navigation etc.&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Add logo and subtitle to menu, lang selector&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;logo&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Custom page logo, title, subtitle&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Full&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;math&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Displays math content with Latex/Katex&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;None, implement math support&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="math"&gt;math&lt;/h3&gt;&#10;&lt;p&gt;Mein Blog soll mathematische Notation unterstützen. Dafür benutze ich &lt;a href="https://katex.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Katex&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_partials/math.html */ -}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/assets/css/katex.min.css&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;defer&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/assets/js/katex.min.js&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;defer&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/assets/js/contrib/auto-render.min.js&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;onload&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;renderMathInElement(document.body);&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; document.&lt;span style="color:#a6e22e"&gt;addEventListener&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;DOMContentLoaded&amp;#34;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt;() {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;renderMathInElement&lt;/span&gt;(document.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;, {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;delimiters&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; [&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;left&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;\\[&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;right&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;\\]&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;}, &lt;span style="color:#75715e"&gt;// block&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;left&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;$$&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;right&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;$$&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;}, &lt;span style="color:#75715e"&gt;// block&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;left&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;\\(&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;right&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;\\)&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;}, &lt;span style="color:#75715e"&gt;// inline&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ],&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;throwOnError&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="cover-image"&gt;cover image&lt;/h3&gt;&#10;&lt;p&gt;Unter der Bezeichnung &lt;code&gt;cover&lt;/code&gt; verstehe ich ein Titelbild eines Blog-Posts. Es wird in Übersichtsseiten, Artikel- und Tag-Listen etc. angezeigt und soll die Inhalte des Blog-posts unterstützend beschreiben. Auch mein &lt;code&gt;RSS&lt;/code&gt;-Feed soll diese Bilder für die Übersicht verschiedener Artikel auf Endgeräten meiner Leserschaft verwenden.&lt;/p&gt;&#10;&lt;p&gt;Es ist besonders stark komprimiert und benötigt wenig Übertragungsbandbreite. Daher ist seine Auflösung gering (&lt;code&gt;440x220&lt;/code&gt; oder &lt;code&gt;640x352&lt;/code&gt;). Für einen einheitlichen Look sind sie zudem stets monochrom.&lt;/p&gt;&#10;&lt;p&gt;Die Markdown-Syntax zum Einbinden des Covers sehe ich wie folgt vor:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;cover_alt&lt;/span&gt;: &lt;span style="color:#f92672"&gt;&amp;#39;Image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Hugo console build output. It shows a full build time below 2sec. (~90% reduction over build time with Jekyll)&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;cover&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/assets/images/migrating-hugo-to-jekyll/hugo-jekyll-build-compare-cover.avif&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Anschließend erstelle ich unter &lt;code&gt;layouts/_partials&lt;/code&gt; die Datei &lt;code&gt;cover.html&lt;/code&gt; und schreibe folgenden Code hinein:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_partials/cover.html */ -}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* Handles cover-image generation */ -}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$cover&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$autoCover&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Site.Params.autoCover&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;index&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;cover&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Resources.GetMatch&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.Cover&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$cover&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Resources.GetMatch&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.Cover&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.RelPermalink&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$cover&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;absURL&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.Cover&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Site.Params.AutoCover&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.Cover&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Resources.GetMatch&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;cover.*&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$cover&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Resources.GetMatch&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;cover.*&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.RelPermalink&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$cover&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;&amp;lt;!-- Cover image found --&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$cover&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-cover&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;alt&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.cover_alt&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;plainify&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Description&lt;/span&gt;&lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;plainify&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;title&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.CoverCredit&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;plainify&lt;/span&gt;&lt;span style="color:#f92672"&gt;|&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Title&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;plainify&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Der Code sucht und extrahiert den Pfad zum Coverbild. Sollte ein valider Pfad gefunden werden, wird es als &lt;code&gt;post-cover&lt;/code&gt; mit entsprechendem CSS angezeigt.&lt;/p&gt;&#10;&lt;p&gt;Dem XML-Template füge ich als erstes Element innerhalb des &lt;code&gt;&amp;lt;description&amp;gt;&lt;/code&gt;-Tags das Coverbild hinzu. Erst danach kommt der eigentliche Artikel. Die meisten Feed Reader nehmen dieses Bild dann als Cover für ihre Übersichten.&lt;/p&gt;&#10;&lt;h2 id="manuelle-anpassungen-layouts"&gt;Manuelle Anpassungen: Layouts&lt;/h2&gt;&#10;&lt;p&gt;Damit sich meine Seite in Hugo weiterhin ganz ähnlich anfüllt wie zuvor in Jekyll muss ich in den Layouts ein paar Änderungen vornehmen. Im Folgenden habe ich sie beschrieben.&lt;/p&gt;&#10;&lt;h3 id="baseofhtml"&gt;baseof.html&lt;/h3&gt;&#10;&lt;p&gt;Hier werden die Basis-Inhalte definiert. Definitionen für Kopf- und Fußzeile werden importiert und Stylesheets eingebunden. Die allermeisten oben beschriebenen Änderungen werden an dieser Stelle importiert.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/baseof.html */ -}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;html&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;lang&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Site.Language&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Param&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;math&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partialCached&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;math.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;block&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;title&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.IsHome&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Site.Title&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Title&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt; :: &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Site.Title&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;head.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/assets/css/gallery.css&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;relURL&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&amp;gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/assets/css/media.css&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;relURL&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$container&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cond&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;$.Site.Params.FullWidthTheme&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;container full&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;cond&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;$.Site.Params.CenterTheme&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;container center&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;container&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$container&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cond&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;$.Site.Params.oneHeadingSize&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34; headings--one-size&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;header.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;block&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;main&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;block&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;footer&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;footer.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;html&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="listhtml"&gt;list.html&lt;/h3&gt;&#10;&lt;p&gt;Für Übersichtsseiten wie &lt;code&gt;Artikel&lt;/code&gt;, &lt;code&gt;Tags&lt;/code&gt;, &lt;code&gt;Projekte&lt;/code&gt; benötige ich eine Listenansicht. Dabei möchte ich je vier Artikel in einer Zeile anzeigen. Jeder Artikel bekommt ein eigenes &amp;ldquo;Kärtchen&amp;rdquo; mit Titelbild (cover), Titel, Kurzbeschreibung und - falls Verfügbar - weiteren Details wie Lesezeit. Hugos &lt;code&gt;paginator&lt;/code&gt; crawlt den entsprechenden Ordner und stellt die Inhalte zusammen, wärend mein Stylesheet &lt;code&gt;list.css&lt;/code&gt; Formatierung und Anzeige übernimmt.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_default/list.html */ -}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;define&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;main&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/assets/css/list.css&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;relURL&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Content&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;index-content&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;posts posts-grid&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;range&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Paginator.Pages&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;article&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post on-list&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;a&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Permalink&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-card&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;aria-label&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Title&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;cover.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-card-body&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;h4&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-title&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Title&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;markdownify&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;h4&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-excerpt&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Description&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;p&lt;/span&gt;&amp;gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Description&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;p&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;and&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Param&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;readingTime&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;eq&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Param&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;readingTime&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-reading-time&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.ReadingTime&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Site.Params.minuteReadingTime&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;min read&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;a&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;article&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;pagination.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="singlehtml"&gt;single.html&lt;/h3&gt;&#10;&lt;p&gt;Meine &lt;code&gt;single,html&lt;/code&gt; ist fast identisch mit der Standardimplementierung von &lt;a href="https://github.com/panr/hugo-theme-terminal/blob/master/layouts/_default/single.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;terminal&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Ich habe lediglich die Möglichkeit hinzugefügt, ein zum Titel gehöriges &amp;ldquo;banner-image&amp;rdquo; zu haben, und importiere die &lt;code&gt;_partial/cover.html&lt;/code&gt;-Vorschrift zum Laden eines Coverbildes.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;article&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.banner&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-banner&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;alt&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Params.banner_alt&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;plainify&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39; &amp;#39;&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bei all den Änderungen könnte man überlegen, ein eigenes Theme zu forken&amp;hellip; 🤔&lt;/p&gt;&#10;&lt;h2 id="qualitätskontrolle"&gt;Qualitätskontrolle&lt;/h2&gt;&#10;&lt;p&gt;Neu ist für meine Website auch, dass ich auf tote Links prüfe. Die Seite ist erwachsen geworden und verfügt über so viele interne und externe Verlinkungen, dass dies manuell nicht mehr sinnvoll durchzuführen ist. Mit welchem Tool ich vorgehe und wie ich dabei Hugo-Eigenheiten berücksichtige, dokumentiert der Artikel zum Thema &lt;a href="https://blog.schallbert.de/hugo-link-checker/"&gt;&lt;code&gt;htmltest&lt;/code&gt;-Konfiguration für Hugo&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="veröffentlichung"&gt;Veröffentlichung&lt;/h2&gt;&#10;&lt;p&gt;Auch für Hugo benötige ich eine automatisierte Pipeline. Sobald ich eine Änderung nach &lt;code&gt;main&lt;/code&gt; schiebe, soll die Webseite automatisch neu gebaut und ausgeliefert werden. Wie das genau funktioniert beschreibe ich im Artikel &lt;a href="https://blog.schallbert.de/build-deploy-hugo-with-actions-docker-caddy/"&gt;Hugo-Website mit Github Actions in Docker bauen und ausliefern&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="leistung"&gt;Leistung&lt;/h2&gt;&#10;&lt;p&gt;Was der ganze Aufwand jetzt gebracht hat? Für &lt;em&gt;user&lt;/em&gt; ist Aussehen und Navigation sehr ähnlich zur Website in Jekyll. Die Verbesserungen machen sich allerdings durch schnellere Ladezeiten, geringere zu übertragende Datenmengen und bessere Übersichtlichkeit aus. Einendetaillierten Vergleich zwischen nehme ich im Artikel &lt;a href="https://blog.schallbert.de/hugo-versus-jekyll-benchmark/"&gt;&lt;em&gt;Jekyll&lt;/em&gt; vs. &lt;em&gt;Hugo&lt;/em&gt; Benchmarking&lt;/a&gt; vor.&lt;/p&gt;&#10;</description></item><item><title>Hugo: Bilder im Textfluss ausrichten</title><link>https://blog.schallbert.de/migrate-jekyll-to-hugo-image-alignment/</link><pubDate>Sat, 29 Aug 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/migrate-jekyll-to-hugo-image-alignment/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-08-29-image-positioning-cover.avif"&#10; class="post-cover"&#10; alt="Image: A screenshot of my blog, showing three images, one each aligned left, center, right. The centered image has no text flow."&#10; title="Hugo: Bilder im Textfluss ausrichten" /&gt;&#10;&lt;h2 id="warum-das-ganze"&gt;Warum das Ganze?&lt;/h2&gt;&#10;&lt;p&gt;Mein Theme kann aus dem klassischen Markdown &lt;code&gt;![Image Alt Text](/path/to/image/)&lt;/code&gt; keine Bildformatierung und -Positionierung ableiten. Das benötige ich aber, um Bilder links, mittig, und rechts zu platzieren - mit und ohne Textfluss. Also muss ich selbst ran und mir etwas programmieren.&lt;/p&gt;&#10;&lt;h2 id="einstieg-in-shortcodes-layouts-css"&gt;Einstieg in shortcodes, layouts, css&lt;/h2&gt;&#10;&lt;p&gt;Einen Überblick verschaffe ich mir auf &lt;a href="https://cshire.xyz/centering-images-in-hugo/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cshire&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und stelle schnell fest, dass mein erster Versuch per &lt;code&gt;/layouts/render-image.html&lt;/code&gt; nicht funktioniert.&lt;/p&gt;&#10;&lt;h3 id="debugging"&gt;Debugging&lt;/h3&gt;&#10;&lt;p&gt;Auf diese Weise stelle ich fest, ob &lt;code&gt;render-image.html&lt;/code&gt; überhaupt gelesen und angewendet wird - und zwar unabhängig von CSS:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;style&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;border: 3px dashed red; padding: 10px; margin: 10px; font-weight: bold;&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RENDER-IMAGE.HTML IS BEING READ!&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das funktioniert. Also muss Hugo irgendwas in der Markdown-Verarbeitung entgehen.&lt;/p&gt;&#10;&lt;h3 id="ursachenbehebung"&gt;Ursachenbehebung&lt;/h3&gt;&#10;&lt;p&gt;Ich sehe nun, dass Hugo zwar das Layout rendert, meine Bilder aber nicht richtig auswertet. Warum? Weil zur Auswertung der Bilder ein Auslöser, genannt &lt;code&gt;shortcode&lt;/code&gt;, fehlt. Shortcodes in Hugo werden mit &lt;code&gt;{{ &amp;lt;shortcode-name&amp;gt; parameter1=&amp;quot;String&amp;quot; parametern=&amp;quot;string&amp;quot; }}&lt;/code&gt; im Markdown angelegt, mittels der entsprechenden Vorschrift &lt;code&gt;/layouts/shortcodes/&amp;lt;shortcode-name&amp;gt;.html&lt;/code&gt; ausgewertet und anschließend durch &lt;code&gt;CSS&lt;/code&gt; interpretiert dargestellt.&lt;/p&gt;&#10;&lt;h2 id="shortcode-imagehtml"&gt;Shortcode: image.html&lt;/h2&gt;&#10;&lt;p&gt;Ich habe mir also ein Shortcode-Template gebastelt, sodass ich dem Bild ein paar weitere Parameter zusätzlich zu Quelldatei &lt;code&gt;src&lt;/code&gt; und Alternativtext &lt;code&gt;alt&lt;/code&gt; mitgeben kann: &lt;code&gt;position, caption, attrid, attrlink&lt;/code&gt;. Position und Titel des Bildes sind selbsterklärend. Eine Attributionsmöglichkeit wollte ich für nicht selbsterstellte Bilder haben. Bei diesen greife ich entweder auf offene Inhalte unter Rechtsverhältnissen wie den &lt;a href="https://en.wikipedia.org/wiki/Creative_Commons" target="_blank" rel="noopener noreferrer" class="external-link"&gt;creative commons&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zu oder besorge mir vorab das schriftliche Einverständnis der Urheberrechtsinhaberschaft.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#75715e"&gt;/* layouts/_shortcodes/image.html */&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;src&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;src&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;alt&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;alt&amp;#34;&lt;/span&gt; | &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;position&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;position&amp;#34;&lt;/span&gt; | &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;center&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;caption&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;caption&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;attrid&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;attrid&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;attrlink&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;attrlink&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#a6e22e"&gt;figure&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;media-frame media-frame--{{ $position }}&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;{{ $src }}&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;alt&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;{{ $alt }}&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;caption&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;figcaption&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;media-caption&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;span&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;caption-text&amp;#34;&lt;/span&gt;&amp;gt;{{ &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;caption&lt;/span&gt; }}&amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;span&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;and&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;attrid&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;attrlink&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;a&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;{{ $attrlink }}&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;attr-link&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;aria&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;label&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;Attribution {{ $attrid }}&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;sup&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;attr-id&amp;#34;&lt;/span&gt;&amp;gt;[{{ &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;attrid&lt;/span&gt; }}]&amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;sup&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;a&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;end&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;figcaption&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;end&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;figure&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diese Vorlage liest die mitgegebenen Parameter aus dem Shortcode aus und packt das Bild &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; in einen Rahmen &lt;code&gt;&amp;lt;figure class=&amp;quot;media-frame&amp;quot;&amp;gt;&lt;/code&gt;. Dieser trägt die Positionsinformation. Sofern ein Bildtitel und/oder eine Attribution vorliegt, wird dies in der &lt;code&gt;figcaption class=&amp;quot;media-caption&amp;quot;&lt;/code&gt; abgebildet.&lt;/p&gt;&#10;&lt;h3 id="verwendung-des-shortcodes"&gt;Verwendung des Shortcodes&lt;/h3&gt;&#10;&lt;p&gt;In Markdown würde ein Bild mit &amp;ldquo;Vollausstattung&amp;rdquo; nun so aussehen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&amp;lt; &lt;span style="color:#a6e22e"&gt;image&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;/path/to/image.jpg&amp;#34;&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;alt&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;Example placeholder image for shortcode demonstration purposes&amp;#34;&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;caption&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;Shortcode demo image&amp;#34;&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;attrid&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;1&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;attrlink&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;https://example.com/&amp;#34;&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;position&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;right&amp;#34;&lt;/span&gt; &amp;gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="migration"&gt;Migration&lt;/h3&gt;&#10;&lt;p&gt;Nur wie komme ich jetzt von den bisher von mir in Jekyll verwendeten &lt;code&gt;![alt](/path/to/image) {: .align-center}&lt;/code&gt; in das oben angegebene Format?&lt;/p&gt;&#10;&lt;p&gt;Mit einem Skript! Dafür erstelle ich mir ein &lt;code&gt;python&lt;/code&gt;-Programm, welches durch jeden meiner Artikel kämmt und dort nach Bildern in der oben angegebenen Formatierung sucht. Bei einem Treffer extrahiert es &lt;code&gt;src, alt, position&lt;/code&gt; und ersetzt den kompletten Ausdruck zu einem Hugo-Shortcode-Äquivalent.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; os&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; re&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; pathlib &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Path&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# spaces/tabs only, no newlines&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;SPACES &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;r&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;[ \t]*&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;IMG_WITH_OPTIONAL_ALIGN &lt;span style="color:#f92672"&gt;=&lt;/span&gt; re&lt;span style="color:#f92672"&gt;.&lt;/span&gt;compile(&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;r&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;!\[([^\]]*?)\]\(([^)]*?)\)&amp;#39;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# alt, src&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; SPACES &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#75715e"&gt;# optional spaces/tabs before decorator&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;r&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;(?:\{:&amp;#39;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; SPACES &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#75715e"&gt;# literal &amp;#34;{:&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;r&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;[^}\n]*?\.align-(center|left|right)\b[^}\n]*?&amp;#39;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# decorator body (no newlines)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;r&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;\})?&amp;#39;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# optional &amp;#34;}&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;convert_image_alignments&lt;/span&gt;(directory):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;repl&lt;/span&gt;(m):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; alt &lt;span style="color:#f92672"&gt;=&lt;/span&gt; m&lt;span style="color:#f92672"&gt;.&lt;/span&gt;group(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;replace(&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#39;&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; src &lt;span style="color:#f92672"&gt;=&lt;/span&gt; m&lt;span style="color:#f92672"&gt;.&lt;/span&gt;group(&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; position &lt;span style="color:#f92672"&gt;=&lt;/span&gt; m&lt;span style="color:#f92672"&gt;.&lt;/span&gt;group(&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;) &lt;span style="color:#f92672"&gt;or&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# return built Hugo shortcode. Escape Python f-string curly brace { with {{.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;{{{{&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;lt; image src=&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;src&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34; alt=&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;alt&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34; position=&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;position&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34; &amp;gt;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;}}}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; root, _, files &lt;span style="color:#f92672"&gt;in&lt;/span&gt; os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;walk(directory):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; file &lt;span style="color:#f92672"&gt;in&lt;/span&gt; files:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; file&lt;span style="color:#f92672"&gt;.&lt;/span&gt;endswith(&lt;span style="color:#e6db74"&gt;&amp;#39;.md&amp;#39;&lt;/span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;continue&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; filepath &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Path(root) &lt;span style="color:#f92672"&gt;/&lt;/span&gt; file&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; content &lt;span style="color:#f92672"&gt;=&lt;/span&gt; filepath&lt;span style="color:#f92672"&gt;.&lt;/span&gt;read_text(encoding&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;utf-8&amp;#39;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; new_content &lt;span style="color:#f92672"&gt;=&lt;/span&gt; IMG_WITH_OPTIONAL_ALIGN&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sub(repl, content)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; new_content &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; content:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; filepath&lt;span style="color:#f92672"&gt;.&lt;/span&gt;write_text(new_content, encoding&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;utf-8&amp;#39;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Updated: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;filepath&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; __name__ &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; project_root &lt;span style="color:#f92672"&gt;=&lt;/span&gt; input(&lt;span style="color:#e6db74"&gt;&amp;#34;Enter the path to your project root: &amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;strip()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;path&lt;span style="color:#f92672"&gt;.&lt;/span&gt;isdir(project_root):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; convert_image_alignments(project_root)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;Conversion complete!&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;Invalid directory path.&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bei den Regexes habe ich mir von &lt;a href="https://mistral.ai/news/mistral-small-4/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Mistral&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; helfen lassen, ansonsten ist das Skript aber einfach gehalten.&lt;/p&gt;&#10;&lt;p&gt;Bilder mit Titel und/oder Attribution waren in Jekyll als &lt;code&gt;figure&lt;/code&gt; deklariert. Auch für diese schreibe ich ein ganz ähnliches Skript und lasse es bei der Migration durchlaufen.&lt;/p&gt;&#10;&lt;h2 id="css"&gt;CSS&lt;/h2&gt;&#10;&lt;p&gt;Kommen wir zur Darstellung der Bilder. Ich erstelle eine seitenfüllende &lt;code&gt;CSS&lt;/code&gt;-Datei &lt;code&gt;/assets/css/media.css&lt;/code&gt;, welche außerdem Größe und Formatierung anderer eingebetteter Medien wie Audio- und Videodateien einheitlich sowohl für die mobile- als auch die Desktopansicht regeln soll. Dafür greift sie auf die in der Vorlage definierten &lt;code&gt;class=&lt;/code&gt;-Attribute zu.&lt;/p&gt;&#10;&lt;p&gt;Ich nehme zur Erläuterung mal einen Ausschnitt aus der Datei heraus: Hier lege ich das Verhalten des &lt;code&gt;media-frame&lt;/code&gt;-Objektes fest. Jedes Asset (Bild, Video, Audio) wird in einem Rahmen eingebettet, welcher der Darstellung die nötige Luft verschafft. Mit dem Attribut &lt;code&gt;position=&lt;/code&gt; wird definiert, wo und wie dargestellt werden soll.&lt;/p&gt;&#10;&lt;p&gt;Dabei sind die Inhalte standardmäßig zentriert und werden solange sie nicht größer sind als die maximale Textbreite in ihrer nativen Auflösung dargestellt.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;media-frame&lt;/span&gt; &lt;span style="color:#f92672"&gt;img&lt;/span&gt;&lt;span style="color:#f92672"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;media-frame&lt;/span&gt; &lt;span style="color:#f92672"&gt;audio&lt;/span&gt;&lt;span style="color:#f92672"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;media-frame&lt;/span&gt; &lt;span style="color:#f92672"&gt;video&lt;/span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;block&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;width&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;auto&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;%&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;height&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;auto&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;margin&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;padding&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;border&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;border-radius&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;media-frame--center&lt;/span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;margin-left&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;auto&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;margin-right&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;auto&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/* Left/Right align only for Non-mobile use */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#66d9ef"&gt;media&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#f92672"&gt;min-width&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;768px&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;media-frame--left&lt;/span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;left&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;%&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;margin-top&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0.35&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;em&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;margin-right&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;margin-bottom&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0.75&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;media-frame--right&lt;/span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;float&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;right&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;%&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;margin-top&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0.35&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;em&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;margin-left&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;margin-bottom&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0.75&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Auf einer Seite ausgerichtete Bilder werden in der Breite begrenzt, denn es soll noch Textfluss drum herum stattfinden. Außerdem werden minimale Abstände zu anderen Objekten definiert, damit sich bei aufeinanderfolgenden Medien nicht Rahmen an Rahmen reibt. In der mobilen Ansicht ist der Platz so beengt, dass Bilder stets zentriert dargestellt werden.&lt;/p&gt;&#10;&lt;h2 id="galerie"&gt;Galerie&lt;/h2&gt;&#10;&lt;p&gt;Für den Fall, dass ich viele Bilder an einer zentralen Stelle anzeigen will, erstelle ich mir zusätzlich einen &lt;code&gt;gallery&lt;/code&gt;-Shortcode und zugehöriges CSS. Die Galerie zeigt Bilder in einem Raster an. Bewegt man die Maus über eines der Bilder, so erscheint es heller als die anderen. Klickt man auf ein Bild, so erscheint es in groß am unteren Rand der Galerie, wobei der oben bereits beschriebene &lt;code&gt;media&lt;/code&gt;-Shortcode wiederverwendet wird.&lt;/p&gt;&#10;&lt;p&gt;Die darzustellenden Bilder werden in der front matter des Markdown-Dokumentes als Liste per &lt;code&gt;src&lt;/code&gt; und &lt;code&gt;alt&lt;/code&gt; angelegt.&#10;Hier ein Ausschnitt aus der Vorlage zur Auswertung des Shortcodes.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#a6e22e"&gt;section&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-gallery&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-gallery__frame&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-gallery__grid&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;role&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;list&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;range&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;i&lt;/span&gt;, &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;img&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;items&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;src&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;img&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;src&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;alt&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;img&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;alt&lt;/span&gt; | &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;printf&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Gallery image %d&amp;#34;&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;add&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;i&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)) &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;thumbURL&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;src&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;p&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;TrimPrefix&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;src&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/assets/&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;with&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;resources&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Get&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;p&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;thumb&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;Fit&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;400x220&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;thumbURL&lt;/span&gt; = &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;thumb&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;RelPermalink&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;end&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;a&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-gallery__thumb&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;#gallery-full-{{ $i }}&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;aria&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;label&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;{{ $alt }}&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#a6e22e"&gt;img&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;{{ $thumbURL }}&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;alt&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;{{ $alt }}&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;hugo-gallery__thumb-img&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;loading&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;lazy&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;a&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{&lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;end&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich iteriere durch alle in der Frontmatter definierten Bilder, begrenze ihre Größe auf handliche &lt;code&gt;400x220px&lt;/code&gt; und ordne sie in einem Raster &lt;code&gt;class=hugo-gallery__grid&lt;/code&gt; an.&lt;/p&gt;&#10;&lt;h2 id="tests"&gt;Tests&lt;/h2&gt;&#10;&lt;p&gt;Das Erstellen der Vorlagen ging verhältnismäßig schnell. Mit &lt;code&gt;CSS&lt;/code&gt; stehe ich aber auf Kriegsfuß. Ständig funkt mir irgendein Override aus einer anderen Datei dazwischen oder ich komme mir selbst mit den Styles im Basis-CSS in die Quere. Daher benötige ich einen Ort zum Testen von Formatierung und Anordnung.&lt;/p&gt;&#10;&lt;p&gt;Daher habe ich eine Bild-testseite erstellt. Neugierig?&#10;&lt;a href="https://blog.schallbert.de/hugo-testing-image-alignment/"&gt;Prüfen der Darstellung von Medien in Hugo&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>htmltest-Konfiguration für Hugo</title><link>https://blog.schallbert.de/hugo-link-checker/</link><pubDate>Mon, 10 Aug 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/hugo-link-checker/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-08-10-htmltest-success-output-cover.avif"&#10; class="post-cover"&#10; alt="Image: An SSH public key randomart image as visual fingerprint for humans to quickly see if public keys match"&#10; title="htmltest-Konfiguration für Hugo" /&gt;&#10;&lt;p&gt;Nun bin ich mit dem &lt;a href="https://blog.schallbert.de/projects/migrating-jekyll-to-hugo/"&gt;Umzug meiner Website zu Hugo&lt;/a&gt; fast fertig. Ich musste mehrere Anpassungen in der Frontmatter jedes Artikel vornehmen, damit sie in Hugo ähnliche Funktionen aufweisen wie zuvor mit Jekyll. Es ergaben sich einige Änderungen in der Ordnerstruktur teils durch die Migration, teils auch durch meine Erfahrungen als Autor, sodass auf dem Weg einige Links in praktisch jedem meiner Artikel zu überarbeiten waren. Inzwischen habe ich fast 340 davon, wenn man die Übersetzungen mitzählt.&lt;/p&gt;&#10;&lt;p&gt;Der schieren Masse der Links komme ich ohne weitere Hilfsmittel nicht bei. Also benötige ich Werkzeug.&lt;/p&gt;&#10;&lt;h2 id="htmltest"&gt;htmltest&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/wjdp/htmltest" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Htmltest&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist ein Hilfsprogramm, das HTML-Dateien nach Problemen durchsuchen kann. Unter anderem spürt es defekte Links auf. Ich installiere es per&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Red Hat-based Linux package manager install command&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install htmltest&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;und wende es auf die von Hugo &amp;ldquo;gebauten&amp;rdquo; HTMl-Dateien im &lt;code&gt;/public&lt;/code&gt;-Verzeichnis an:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;htmltest public/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;✘✘✘ failed in 2.890119823s&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2612&lt;/span&gt; errors in &lt;span style="color:#ae81ff"&gt;337&lt;/span&gt; documents&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das sind aber ganz schön viele Fehler! Die Darreichungsform erscheint mir außerdem ziemlich unübersichtlich, siehe Bild.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-08-10-htmltest-error-note.avif" alt="Image: Output of a htmltest report in the console. It is completely filled with text. There are different error messages like &amp;#39;name resolution error&amp;#39;, &amp;#39;missing trailing slash&amp;#39; and so on. It is nowhere easy to read but mentions the source path of the issue."&gt;&lt;/figure&gt;&#10;&lt;h3 id="ausgabe-in-datei-umleiten"&gt;Ausgabe in Datei umleiten&lt;/h3&gt;&#10;&lt;p&gt;Folglich tue ich mich erstmal ziemlich schwer mit diesem Werkzeug. Ich hatte angenommen, dass mir ein paar Links kaputtgegangen sind und nicht mit tausenden Fehlern gerechnet. Also fiel ich dem typischen &lt;a href="https://en.wikipedia.org/wiki/Not_invented_here" target="_blank" rel="noopener noreferrer" class="external-link"&gt;not-invented-here&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Syndrom anheim und programmierte selbst ein Werkzeug in &lt;em&gt;python&lt;/em&gt;, welches sich im Nachhinein als nicht gut genug entpuppte. Kurzgesagt:&lt;/p&gt;&#10;&lt;p&gt;Htmltest ist ein tolles und unglaublich schnelles Werkzeug. Wenn man dessen Ausgabe leserlich macht und richtig deutet, kann man damit ziemlich glücklich werden. Mit ein bisschen Herumprobieren gelange ich zu folgendem Befehl:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;htmltest public/ 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; | ansi2html &amp;gt; htmltest-output.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;&#10;&lt;p&gt;Bedeutung: &amp;ldquo;Führe &lt;em&gt;htmltest&lt;/em&gt; im &lt;code&gt;/public/&lt;/code&gt;-Ordner aus. Leite Fehler aus &lt;code&gt;stderr&lt;/code&gt; in die Ausgabe &lt;code&gt;stdout&lt;/code&gt; um. Reiche die Ausgabe an &lt;em&gt;ansi2html&lt;/em&gt;&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; weiter. Schreibe dessen Ausgabe in eine Datei namens &lt;code&gt;htmltest-output.html&lt;/code&gt;.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-08-10-htmltest-ansi2html-output.avif" alt="Image: Output of a htmltest report in html after piping through ansi2html, opened with a browser. The lines are formatted properly and have colour coding. Looks neat."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Das sieht doch so schon viel besser aus!&lt;/p&gt;&#10;&lt;h3 id="fehlerkategorien"&gt;Fehlerkategorien&lt;/h3&gt;&#10;&lt;p&gt;Die mit &lt;em&gt;htmltest&lt;/em&gt; gefundene Fehler lassen sich für meine Zwecke in ein paar wenige Kategorien sortieren:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Hugo-intern bedingte Fehler wie durch &lt;code&gt;livereload.js&lt;/code&gt; verursachte &lt;code&gt;target does not exist&lt;/code&gt;-Meldungen.&lt;/li&gt;&#10;&lt;li&gt;Unvollständige Links, die zwar in der Praxis funktionieren, aber &lt;code&gt;href lacks trailing slash&lt;/code&gt;-Fehler erzeugen. Diese Art Fehler machen bei mir den Löwenanteil der berichteten Probleme aus.&lt;/li&gt;&#10;&lt;li&gt;Lookup-Fehler &lt;code&gt;GET &amp;lt;src&amp;gt; [...] failure in name resolution&lt;/code&gt;, bei fehlender Verbindung, unzureichenden Rechten, aktiver Bot-Abwehr oder wenn der Zielserver offline ist&lt;/li&gt;&#10;&lt;li&gt;Harte, durch defekte Links erzeugte &lt;code&gt;target does not exist&lt;/code&gt;-Fehler. Genau diese wollte ich eigentlich beheben.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="das-log-bereinigen"&gt;Das Log bereinigen&lt;/h3&gt;&#10;&lt;p&gt;Wie bekomme ich nun nur noch Fehler der vierten Kategorie angezeigt? Durch ein paar Änderungen in den Werkeugen und die Beseitigung der Trailing-Slash-Fehler.&lt;/p&gt;&#10;&lt;p&gt;Fehler der Kategorie 1, die durch Hugos nützliche &lt;code&gt;livereload&lt;/code&gt;-Funktion hervorgerufen werden, sehen so aus:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt; target does not exist --- en/tags/tools/index.html --&amp;gt; /livereload.js?mindelay=10&amp;amp;v=2&amp;amp;port=1313&amp;amp;path=livereload&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;LiveReload ist diejenige Funktion, die mir ständiges &lt;code&gt;F5&lt;/code&gt;-Tippen im Browserfenster erspart, wenn Hugo eine meiner Änderungen nach dem Speichern der Markdown-Datei baut. Für den Test kann ich es jedoch kurz deaktivieren. Die folgende Lösung reduziert die Anzahl erzeugter Fehler um eins pro html-Seite.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;hugo server --disableLiveReload&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Lookup-Fehler der Kategorie 3 vermeide ich durch einen Fokus auf interne Links worüber ich die vollständige Kontrolle habe. Hierbei hilft mir der Parameter &lt;code&gt;-s, --skip-external&lt;/code&gt; in &lt;em&gt;htmltest&lt;/em&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;htmltest -s public/ 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; | ansi2html &amp;gt; htmltest-output.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;✘✘✘ failed in 494.7678ms&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;446&lt;/span&gt; errors in &lt;span style="color:#ae81ff"&gt;337&lt;/span&gt; documents&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tote externe Links kann ich zu einem späteren Zeitpunkt beheben; nun will ich erstmal die durch die Migration entstandenen Probleme lösen.&lt;/p&gt;&#10;&lt;h2 id="missing-trailing-slash-fehlerbehebung-per-markup"&gt;&amp;ldquo;Missing trailing slash&amp;rdquo;: Fehlerbehebung per Markup&lt;/h2&gt;&#10;&lt;p&gt;Mit den vielen &lt;code&gt;href lacks trailing slash&lt;/code&gt;-Fehlern tue ich mich lange zeit schwer, müsste ich doch an fast jeden Markdown-Link in jedem Artikel ein &lt;code&gt;/&lt;/code&gt; anfügen. Zum Glück finde ich eine Lösung in meinem Markup &lt;code&gt;render-link.html&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_markup/render-link.html */ -}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Destination&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;safeURL&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_external&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;or&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.HasPrefix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;http://&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.HasPrefix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;https://&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_anchor&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.HasPrefix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_asset&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.HasPrefix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/assets/&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[...]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* Add / to internal paths that have no file extension */ -}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;and&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_external&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_anchor&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_asset&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$parsed&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;urls&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Parse&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;and&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$parsed&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.HasSuffix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$parsed&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Path&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;))&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;eq&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;path&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Ext&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$parsed&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Path&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;printf&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;%s/&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[...]&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;&#10;&lt;p&gt;Bedeutung: Interne Links, die weder auf Überschriften &lt;code&gt;is_anchor&lt;/code&gt; noch Mediendateien &lt;code&gt;is_asset&lt;/code&gt; verweisen, bekommen genau dann ein &lt;code&gt;/&lt;/code&gt; angehangen, wenn sie eine korrekte Syntax aufweisen &lt;code&gt;parsed&lt;/code&gt; und noch keinen Slash am Ende der URL vorweisen können.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Dies löst 95% der Fehlermeldungen durch weniger hässliches &lt;code&gt;html&lt;/code&gt;. Die restlichen 5% hole ich mir durch Anhängen von ein paar Slashes in der Hugo-Konfiguration. Beispiele:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# hugo.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;url&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;/en/&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# /en -&amp;gt; /en/ &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;logoHomeLink&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;/en/&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# /en -&amp;gt; /en/&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Fertig! Übrig bleiben nur noch harte, interne Fehler.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;htmltest -s public/ 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; | ansi2html &amp;gt; htmltest-output.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;✘✘✘ failed in 359.28ms&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;102&lt;/span&gt; errors in &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt; documents&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="manuelles-suchen-und-beheben"&gt;Manuelles Suchen und Beheben&lt;/h2&gt;&#10;&lt;p&gt;Was nun folgt ist reine Fleißarbeit. Ich öffne das Zieldokument (also jenes, auf welches per Link verwiesen wird) und schaue mir an, ob zum Beispiel Überschriftenlinks zwischen verschiedenen Sprachen uneinheitlich sind. Falls ja, füge ich individuelle Anchor-Links an die Überschrift an.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-md" data-lang="md"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### Überschrift {#heading3}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Falls in der Zieldatei alles paletti ist, öffne ich zusätzlich alle Quelldateien, die den defekten Link verwenden. Dort korrigiere ich den Link manuell. In den meisten Fällen befindet sich die Zieldatei inzwischen in einem anderen Ordner, oder der Sprachpräfix ist falsch gesetzt.&lt;/p&gt;&#10;&lt;h2 id="das-ergebnis"&gt;Das Ergebnis&lt;/h2&gt;&#10;&lt;p&gt;Nach ein paar Stunden anstrengender und dröger Handrbeit werde ich belohnt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;htmltest public/ -s 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Skipping the checking of external links.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;htmltest started at 10:51:23 on public&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;========================================================================&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;✔✔✔ passed in 238.970354ms&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tested &lt;span style="color:#ae81ff"&gt;338&lt;/span&gt; documents&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bald kann ich mit meiner neuen alten Seite online gehen!&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;&lt;a href="https://packages.debian.org/en/sid/colorized-logs" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ansi2html bzw. colorized-logs&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist ein Hilfsprogramm, welches Konsolenausgabe inkl. Formatierung und Textfarben in &lt;code&gt;html&lt;/code&gt; umwandelt.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Hugo Multi-Lang: Interne Permalinks</title><link>https://blog.schallbert.de/hugo-create-language-specific-permalinks/</link><pubDate>Mon, 27 Jul 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/hugo-create-language-specific-permalinks/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-07-07-defaultcontentlanguage-in-hugo-config.avif"&#10; class="post-cover"&#10; alt="Image: A section in the hugo.yml file setting defaultContentLanguage and related parameters."&#10; title="Hugo Multi-Lang: Interne Permalinks" /&gt;&#10;&lt;p&gt;Wer in Hugo wie ich einen mehrsprachigen Blog baut, könnte folgendes Problem kennen:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Interne Links in Artikeln verschiedener Sprachen verweisen nur auf Seiten in der Standard-Sprache.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="konfiguration"&gt;Konfiguration&lt;/h2&gt;&#10;&lt;p&gt;Grundlage für das Problem ist die folgende Einstellung:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# hugo.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;defaultContentLanguage&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;de&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;defaultContentLanguageInSubdir&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;disableDefaultLanguageRedirect&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Meine Standardsprache ist Deutsch und zugehörige Artikel werden in das Hauptverzeichnis des Blogs gebaut. Eine Weiterleitung in das Sprachverzeichnis &lt;code&gt;/de&lt;/code&gt; findet nicht statt.&lt;/p&gt;&#10;&lt;p&gt;Die Verzeichnisstruktur lautet:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── content&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── de&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── about.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── announcements&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── legal.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── posts&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ ├── privacy.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ └── projects&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── en&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── about.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── legal.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── posts&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── privacy.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── projects&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="das-problem-mit-der-standardsprache"&gt;Das Problem mit der Standardsprache&lt;/h2&gt;&#10;&lt;p&gt;Ich halte Artikel durch Nutzung von &lt;a href="https://gohugo.io/content-management/urls/#slug" target="_blank" rel="noopener noreferrer" class="external-link"&gt;slug&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; in meiner Standardsprache unter Adressen erreichbar wie &lt;code&gt;example.com/my-post&lt;/code&gt;, &lt;code&gt;example.com/my-2nd-post/&lt;/code&gt;.&#10;In der Zweitsprache hingegen erscheinen sie als &lt;code&gt;example.com/en/my-post&lt;/code&gt;, &lt;code&gt;example.com/en/my-2nd-post/&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Wenn ich zwei Artikel nun intern verlinken möchte, dann verweist der Link stets auf den Artikel meiner Standardsprache.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# English version of my post&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# example.com/en/my-post&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;slug&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;my-post&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;Post&amp;#39;s English content&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;2nd](/my-2nd-post) &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich erreiche damit statt des gewünschten &lt;code&gt;/en/my-2nd-post&lt;/code&gt; nur die deutsche Version &lt;code&gt;/my-2nd-post&lt;/code&gt;. Das finde ich blöd, denn so müsste ich jeden Link in allen Artikeln manuell anpassen. Leider scheint Hugo dafür keine eingebaute Lösung mitzubringen. Also erstelle ich mir eine Vorschrift, wie Links gerendert werden sollen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_markup/render-link.html */ -}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="lösung-sprachabhängige-link-prefixes"&gt;Lösung: Sprachabhängige Link-Prefixes&lt;/h2&gt;&#10;&lt;p&gt;Dort soll folgendes passieren: Wenn beim Bauen ein relativer Link innerhalb der Seite gefunden wird, so ist genau dann ein Sprachpräfix wie &lt;code&gt;/en&lt;/code&gt; vor diesen Link zu setzen, wenn die Sprache der zu rendernden Seite nicht der Standardsprache entspricht (denn für letztere benötige ich wegen &lt;code&gt;defaultContentLanguageInSubdir: false&lt;/code&gt; keinen Sprachpräfix).&lt;/p&gt;&#10;&lt;h3 id="standardsprache-auslesen"&gt;Standardsprache auslesen&lt;/h3&gt;&#10;&lt;p&gt;Nur wie komme ich an die Standardsprache? Die Parameter &lt;code&gt;defaultContentLanguage&lt;/code&gt; sind in Templates nicht zugänglich. Nach längerer Recherche finde ich eine passende Lösung für das Problem in der &lt;a href="https://discourse.gohugo.io/t/is-there-a-way-to-retrieve-the-value-of-defaultcontentlanguage/9643" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hugo community&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;: Die Standardsprache ist meist die erste Sprache des gewichtsbasierten Index (welcher die &lt;a href="https://gohugo.io/configuration/languages/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Sprachen nach Reihenfolge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erscheinen lässt): &lt;code&gt;index .Page.Site.Home.AllTranslations 0 &lt;/code&gt; Wenn ich diesen nun auslese, ist die &amp;ldquo;erste&amp;rdquo; meine Standardsprache.&lt;/p&gt;&#10;&lt;h3 id="code-für-render-linkhtml"&gt;Code für render-link.html&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_markup/render-link.html */ -}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Destination&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;safeURL&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$currentLang&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Page.Language.Locale&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_external&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;or&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.HasPrefix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;http://&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.HasPrefix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;https://&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_anchor&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.HasPrefix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_asset&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.HasPrefix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/assets/&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_lang_prefixed&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.HasPrefix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;printf&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/%s/&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$currentLang&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_anchor&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;printf&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;%s%s&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Page.RelPermalink&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_external&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;and&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_lang_prefixed&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_asset&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Page.Language.IsDefault&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;printf&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/%s%s&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$currentLang&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* Add / to internal paths that have no file extension */ -}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;and&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_external&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_anchor&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_asset&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$parsed&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;urls&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Parse&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;and&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$parsed&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.HasSuffix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$parsed&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Path&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;))&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;eq&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;path&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Ext&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$parsed&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Path&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;printf&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;%s/&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;a&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;relURL&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_external&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;target&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;_blank&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;noopener noreferrer&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;external-link&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Text&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;safeHTML&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_external&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;span&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;external-link-icon&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;aria-hidden&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;&amp;gt;↗&amp;lt;/&lt;span style="color:#f92672"&gt;span&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;a&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Solange die Standardsprache und der Gewichtsindex übereinstimmen, d.h. die Standardsprache ganz oben auftaucht, funktioniert dies prima.&lt;/p&gt;&#10;&lt;h3 id="zusatzfunktionen"&gt;Zusatzfunktionen&lt;/h3&gt;&#10;&lt;p&gt;Was der Code sonst noch macht:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Prüfung, ob Links nach &amp;ldquo;extern&amp;rdquo;, also von meiner Site weg gehen. Jene bekommen dann ein eigenes Fenster und den Indikator &lt;code&gt;↗&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Prüfung, ob der Link auf eine Überschrift &amp;ldquo;anchor&amp;rdquo; führt. Details dazu siehe unten.&lt;/li&gt;&#10;&lt;li&gt;Prüfung, ob der Link zu einer (Medien-)Datei &amp;ldquo;asset&amp;rdquo; führt. Asset-Links werden nicht modifiziert.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Handelt es sich um einen Überschriftslink, so wird dieser gegebenenfalls auf die vollständige URL ergänzt. Das geschieht nach folgendem Schema: &lt;code&gt;#heading --&amp;gt; /&amp;lt;opt_lang&amp;gt;/&amp;lt;opt_category&amp;gt;/&amp;lt;title&amp;gt;/#heading&lt;/code&gt;&lt;/p&gt;&#10;&lt;h2 id="permalinks"&gt;Richtig auflösende Permalinks auf Überschriften&lt;/h2&gt;&#10;&lt;p&gt;Auch für hochspezielle Permalinks innerhalb einer Seite habe ich eine Lösung gefunden. Nehmen wir an, ich habe verschiedensprachige Überschriften :&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# German&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# content/de/posts/yyyy-mm-dd-hugo-create-language-specific-permalinks.md&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;description&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Mit _markup interne Links richtig routen&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Hugo Multi-Lang: Interne Permalinks&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;slug&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;hugo-create-language-specific-permalinks&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Richtig auflösende Permalinks auf Überschriften {#permalinks}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;Lorem Ipsum...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# English&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /content/en/posts/yyyy-mm-dd-hugo-create-language-specific-permalinks.md&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;description&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Routing internal links with _markup&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Hugo Multi-Lang: Internal Permalinks&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;slug&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;hugo-create-language-specific-permalinks&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## Correct Permalinks to headings {#permalinks}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;Lorem Ipsum...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dann sollte ein Klick auf &lt;a href="https://blog.schallbert.de/hugo-create-language-specific-permalinks/#permalinks"&gt;diesen Link zurück zur Überschrift #permalinks&lt;/a&gt; sprachunabhängig wieder zurück zur Überschrift führen. Schlüssel für diese Lösung ist das Einführen einer Überschriftsdefinition (&lt;a href="https://www.markdownlang.com/cheatsheet/headings.html#heading-ids-extended-syntax" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Heading-ID&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) in Markdown, in diesem Fall &lt;code&gt;{#permalinks}&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="funktioniert-so-noch-nicht"&gt;Funktioniert so noch nicht.&lt;/h3&gt;&#10;&lt;p&gt;Weil &lt;code&gt;#permalinks&lt;/code&gt; auf &lt;code&gt;&amp;lt;blog's baseurl&amp;gt;/#permalinks&lt;/code&gt; verweist und somit der Link auf die aktuelle Seite fehlt. Doch auch das bekommen wir hin: Ich erweitere den Link-Renderer um ein paar Zeilen Code und gebe die aktuelle Seiten-URL als Präfix hinzu.&lt;/p&gt;&#10;&lt;h3 id="code-für-render-linkhtml-mit-unterstützung-für-überschriften"&gt;Code für render-link.html mit Unterstützung für Überschriften&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_anchor&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;strings&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.HasPrefix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$is_anchor&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;printf&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;%s%s&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Page.RelPermalink&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$url&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nun wird &lt;code&gt;(#permalinks)&lt;/code&gt;erweitert zu &lt;code&gt;/hugo-create-language-specific-permalinks/#permalinks&lt;/code&gt;, sodass der Link endlich zum Ziel führt.&lt;/p&gt;&#10;</description></item><item><title>Scheitern von SSH-Verbindungen</title><link>https://blog.schallbert.de/ssh-typical-issues/</link><pubDate>Fri, 12 Jun 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/ssh-typical-issues/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-06-12-ssh-errors-thumb.avif"&#10; class="post-cover"&#10; alt="Image: A list of different SSH error messages. Some very common, but a few have rare causes."&#10; title="Scheitern von SSH-Verbindungen" /&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-06-12-ssh-icon.svg" alt="Image: terminal logo image from Aegis simple image pack, framing the topic of this post."&gt;&lt;/figure&gt;&#10;In diesem kurzen Artikel geht es um Fehler beim Aufbau einer SSH-Verbindung. Egal ob andere Rechner, Server oder Dienste wie Versionsverwaltung - die Fehlermeldungen und zugehörige Lösungen sind meist gleich. Weiterlesen lohnt sich allerdings nur, wenn gängige Hilfeseiten wie &lt;a href="https://docs.github.com/en/authentication/troubleshooting-ssh" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Githubs troubleshooting-ssh&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; nicht die erwünschte Lösung brachte.&lt;/p&gt;&#10;&lt;h2 id="problem-no-route-to-host"&gt;Problem: &amp;ldquo;No Route To Host&amp;rdquo;&lt;/h2&gt;&#10;&lt;p&gt;Die Konsolenausgabe zeigt (im lokalen Netz):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ ssh 192.168.0.27&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh: connect to host 192.168.0.27 port 22: No route to host&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Oder (in einem entfernten Netz)&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ ssh 1.2.3.4&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh: connect to host 1.2.3.4 port 22: Network is unreachable&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ursache ist sehr wahrscheinlich eine falsch eingegebene IP oder eine fehlgeschlagene DNS-Namensauflösung. Im lokalen Netz ist ein realistischer Grund, dass der Teilnehmer abgeschaltet ist und der Router ihn daher nicht erreichen kann. Abhilfe schafft meist eine Korrektur der Zieladresse, Einschalten des Ziels oder Update der Routing-Tabelle.&lt;/p&gt;&#10;&lt;h2 id="problem-connection-refused"&gt;Problem: Connection Refused&lt;/h2&gt;&#10;&lt;p&gt;Konsolenausgabe:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git push gitea&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh: connect to host git.schallbert.de port 22: Connection refused&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fatal: Could not read from remote repository.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hier kann nicht mal eine Verbindung zum Ziel hergestellt werden. Man ist also &amp;ldquo;vor&amp;rdquo; der Authentisierung schon gescheitert. Meiner Erfahrung nach werden solche Fehler aus drei Gründen erzeugt:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Der Host ist noch nicht bereit. Er bootet beispielsweise gerade (Netzwerkkarte ist an) aber der SSH-Dienst ist noch nicht hochgefahren. Hier hilft meist: abwarten.&lt;/li&gt;&#10;&lt;li&gt;Eine Firewall, ein Rate Limiter, oder Dienste wie &lt;em&gt;fail2ban&lt;/em&gt; weisen die Verbindung ab (&lt;code&gt;REJECT&lt;/code&gt;). Die Gründe hierfür sind meist zu viele erfolglose Anmeldeversuche oder eine fehlgeleitete Bot-Erkennung. In seltenen Fällen kann es aber auch sein, dass die eigene IP-Adresse auf einer &amp;ldquo;deny-list&amp;rdquo; auftaucht, die von den Zielen eingelesen wurde. Auch hier empfiehlt sich: warten.&lt;/li&gt;&#10;&lt;li&gt;Der angesprochene Dienst erwartet einen anderen Port als den SSH-Standard &lt;code&gt;Port 22&lt;/code&gt;. Somit stimmen User/Ziel-Adresse/Port nicht überein.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Das Port-Problem lässt sich leicht lösen.&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-06-12-gitea-server-configuration-ssh-port.avif" alt="Image: Gitea showing SSH port IDs under /admin/config/Server_Configuration/SSH_Configuration, Port:222, Listen Port: 22"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Herausfinden des &amp;ldquo;Listen Port&amp;rdquo; auf dem Host (Server Configuration / SSH). Auf dem Bild ist dargestellt, wie die Einstellung auf meiner Gitea-Instanz aussieht.&#10;Alternativ genügt meist ein Blick in die Konfigurationsdatei des entsprechenden Services.&lt;/li&gt;&#10;&lt;li&gt;Die SSH-Konfiguration unter &lt;code&gt;~./ssh/config&lt;/code&gt; mit einer konkreten Port-Nummer versehen. Beispiel:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /.ssh/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Host gitserver&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Hostname git.schallbert.de&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Port &lt;span style="color:#ae81ff"&gt;222&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="problem-too-many-authentication-failures"&gt;Problem: Too Many Authentication Failures&lt;/h2&gt;&#10;&lt;p&gt;Konsolenausgabe:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git pull&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Received disconnect from &amp;lt;ipAddress&amp;gt; port &amp;lt;portId&amp;gt;: Too many authentication failures&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wir haben nun also eine Verbindung zum Server 😁, aber der mag uns nicht reinlassen 😨. Immerhin wird mitgeteilt, warum: Er meint, wir hätten zu viele Schlüssel an seiner Haustür ausprobiert.&lt;/p&gt;&#10;&lt;h3 id="server-anmeldelimit-prüfen"&gt;Server-Anmeldelimit prüfen&lt;/h3&gt;&#10;&lt;p&gt;Dieser Fehler tritt auf, wenn das Server-Limit der Anmeldeversuche gerissen wird. Der Wert beträgt beim Service &lt;code&gt;sshd&lt;/code&gt; beispielsweise standardmäßig &lt;a href="https://www.baeldung.com/linux/too-many-authentication-failures-error" target="_blank" rel="noopener noreferrer" class="external-link"&gt;MaxAuthTries = 6&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Hat man viele Schlüssel für mehrere Verbindungen abgespeichert oder verwendet wie ich neuerdings &lt;a href="https://blog.schallbert.de/hardware-security-keys-yubikey/"&gt;Hardware Security Keys&lt;/a&gt; und legt Zweitschlüssel an (also &lt;code&gt;ssh-keys.count*2&lt;/code&gt;), wird dieser Wert schnell überschritten.&lt;/p&gt;&#10;&lt;p&gt;Fies bei diesem Fehler ist außerdem, dass er nur bei bestimmten Services &amp;ldquo;unten&amp;rdquo; in des Agents Schlüsselbund auftritt: unter den ersten sechs dürfen ja fünf Schlüssel falsch sein. Zudem schützt einen nicht, wenn man die Schlüssel in der &lt;code&gt;~./ssh/config&lt;/code&gt; korrekt hinterlegt hat. Der Agent nimmt stets alle Schlüssel zum Probieren mit, wenn man es ihm nicht explizit verbietet.&lt;/p&gt;&#10;&lt;p&gt;Das Verbot erteilen wir per &lt;code&gt;IdentitiesOnly yes&lt;/code&gt;. Hiermit wird dem Agent klar gesagt, dass er nur die für diesen Host explizit angegebenen Identities (Also Schlüsseldateien oder Nutzername/Passwort) verwenden soll.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /.ssh/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host gitserver&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Hostname git.schallbert.de&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Port &lt;span style="color:#ae81ff"&gt;222&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; User &amp;lt;user&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PreferredAuthentications publickey&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IdentityFile &amp;lt;path_to_private_key1&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Identityfile &amp;lt;path_to_private_key2&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IdentitiesOnly yes&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="git-config-überprüfen"&gt;Git-Config überprüfen&lt;/h3&gt;&#10;&lt;p&gt;Diesen Fehler bekommt man auch, wenn die Git-Konfiguration des Repository unter &lt;code&gt;&amp;lt;reponame&amp;gt;/.git/config&lt;/code&gt; nicht mit dem in der SSH-Konfiguration &lt;code&gt;~/.ssh/config&lt;/code&gt; hinterlegten Datensatz für Server und Repository übereinstimmt. Ändert man die SSH-Konfiguration, so sind alle auf diesem Server bereitgehaltenen Repo-Konfigurationen ebenfalls zu ändern.&lt;/p&gt;&#10;&lt;p&gt;Beispiel:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# &amp;lt;reponame&amp;gt;/.git/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;remote &lt;span style="color:#e6db74"&gt;&amp;#34;origin&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ssh://git@git.schallbert.de:222/schallbert/&amp;lt;reponame&amp;gt;.git&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;fetch &lt;span style="color:#f92672"&gt;=&lt;/span&gt; +refs/heads/*:refs/remotes/origin/*&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;branch &lt;span style="color:#e6db74"&gt;&amp;#34;main&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;remote &lt;span style="color:#f92672"&gt;=&lt;/span&gt; origin&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;merge &lt;span style="color:#f92672"&gt;=&lt;/span&gt; refs/heads/main&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;vscode-merge-base &lt;span style="color:#f92672"&gt;=&lt;/span&gt; origin/main&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In diesem Falle wird die &lt;code&gt;gitserver&lt;/code&gt;-Konfiguration gar nicht verwendet, sondern sich direkt einwählt. Dies führt dazu, dass der Agent kein &lt;code&gt;IdentitiesOnly yes&lt;/code&gt; Flag mitgeliefert bekommt und er Schlüssel durchprobiert, bis der Server abwinkt. Lösung:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;remote &lt;span style="color:#e6db74"&gt;&amp;#34;origin&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; git@gitserver:schallbert/&amp;lt;reponame&amp;gt;.git&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;fetch &lt;span style="color:#f92672"&gt;=&lt;/span&gt; +refs/heads/*:refs/remotes/origin/*&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="problem-permission-denied-publickeypassword"&gt;Problem: Permission Denied (publickey,password)&lt;/h2&gt;&#10;&lt;p&gt;Konsolenausgabe:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ ssh -T git@gitserver&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git@git.schallbert.de: Permission denied &lt;span style="color:#f92672"&gt;(&lt;/span&gt;publickey,password&lt;span style="color:#f92672"&gt;)&lt;/span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dieses Problem kann leider viele verschiedene Ursachen haben. Ein paar räumt Githubs &lt;a href="https://docs.github.com/en/authentication/troubleshooting-ssh/error-permission-denied-publickey" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bereits aus dem Weg, darunter:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;sudo&lt;/code&gt; verwendet&lt;/li&gt;&#10;&lt;li&gt;Falscher Server&lt;/li&gt;&#10;&lt;li&gt;nicht den &lt;code&gt;git&lt;/code&gt;-user verwendet&lt;/li&gt;&#10;&lt;li&gt;Falschen Schlüssel verwendet (client)&lt;/li&gt;&#10;&lt;li&gt;Schlüssel nicht hinterlegt (server)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Die Ausgabe bei falschem Username sieht so aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ ssh -T mit@gitserver&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mit@git.schallbert.de: Permission denied &lt;span style="color:#f92672"&gt;(&lt;/span&gt;publickey&lt;span style="color:#f92672"&gt;)&lt;/span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Eine weitere Ursache kann eine fehlende Abstimmung zwischen der Konfigurationsdatei &lt;code&gt;~./ssh/config&lt;/code&gt; und dem im &lt;em&gt;git client&lt;/em&gt; hinterlegten Remote-Adresse liegen. Sind diese nicht deckungsgleich, wird eine eventuell bestehende Konfiguration gar nicht erst benutzt.&lt;/p&gt;&#10;&lt;p&gt;Eine Überprüfung kann man mit &lt;code&gt;git remote -v&lt;/code&gt; durchführen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git remote -v&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;origin&#9;ssh://git@git.schallbert.de:222/schallbert/blog.git &lt;span style="color:#f92672"&gt;(&lt;/span&gt;fetch&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;origin&#9;ssh://git@git.schallbert.de:222/schallbert/blog.git &lt;span style="color:#f92672"&gt;(&lt;/span&gt;push&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Korrekt wäre mit der verbesserten Konfigurationsdatei (Ports eindeutig gesetzt, Identity Files definiert und auf die Angegebenen beschränkt):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Update link to server&amp;#39;s git repository, use host alias from config file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git remote update origin git@gitserver:schallbert/blog.git&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Now check if the update was effective&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git remote -v&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;origin&#9;git@gitserver:schallbert/blog.git &lt;span style="color:#f92672"&gt;(&lt;/span&gt;fetch&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;origin&#9;git@gitserver:schallbert/blog.git &lt;span style="color:#f92672"&gt;(&lt;/span&gt;push&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="problem-agent-refused-operation"&gt;Problem: Agent Refused Operation&lt;/h3&gt;&#10;&lt;p&gt;Konsolenausgabe:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;local-setup&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git push --set-upstream origin local-setup &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sign_and_send_pubkey: signing failed &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; ED25519-SK &amp;lt;path-to-private-key&amp;gt; from agent: agent refused operation&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diesen Fehler sehe ich erst, seitdem ich &lt;a href="https://blog.schallbert.de/hardware-security-key-add-ssh/"&gt;Hardware Security Keys für ssh verwende&lt;/a&gt;. Aus meiner Sicht kann er zwei Ursachen haben. Beide sind zum Glück leicht zu beheben:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Der Agent kennt den Schlüssel nicht. Der Befehl &lt;code&gt;ssh-add&lt;/code&gt; löst das Problem. Prüfung per &lt;code&gt;ssh-add -l&lt;/code&gt; | grep &amp;ldquo;&amp;lt;your-key&amp;rsquo;s-comment&amp;gt;&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Der angeforderte Schlüssel ist nicht da. Kann bei HSK und HSM passieren, falls sie nicht eingesteckt sind, nicht richtig auf dem NFC-Lesegerät platziert sind oder das HSM im Netzwerk nicht verfügbar ist. Oder wenn manuell im Verzeichnis &lt;code&gt;~./ssh/config&lt;/code&gt; gearbeitet wurde und der Schlüssel nun umbenannt oder gelöscht ist.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item><item><title>Hardware Security Key: SSH einrichten</title><link>https://blog.schallbert.de/hardware-security-key-add-ssh/</link><pubDate>Sun, 03 May 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/hardware-security-key-add-ssh/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-ssh-randomart_thumb.avif"&#10; class="post-cover"&#10; alt="Image: An SSH public key randomart image as visual fingerprint for humans to quickly see if public keys match"&#10; title="Hardware Security Key: SSH einrichten" /&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;In meinem Post über &lt;a href="https://blog.schallbert.de/hardware-security-keys-yubikey/"&gt;Hardware Security Keys&lt;/a&gt; schrieb ich, dass ich möglichst viele meiner Verbindungen passwortlos mit meinem neuen HSK absichern möchte. Ich verwende &lt;em&gt;SSH&lt;/em&gt;-Verbindungen für verschiedene Dienste und Zwecke: Ich greife auf meinen &lt;a href="https://en.wikipedia.org/wiki/Virtual_private_server" target="_blank" rel="noopener noreferrer" class="external-link"&gt;VPS&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zu, benutze Dienste wie Github, Gitea sowie meine eigene &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/"&gt;Gitea-Instanz&lt;/a&gt; und lasse die Erzeugung von Backups im lokalen Netz über &lt;a href="https://linuxize.com/post/how-to-use-scp-command-to-securely-transfer-files/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;scp&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; laufen, welches das SSH-Protokoll verwendet.&lt;/p&gt;&#10;&lt;h2 id="ssh-zugriff-für-hardware-security-keys-einrichten"&gt;SSH-Zugriff für Hardware Security Keys einrichten&lt;/h2&gt;&#10;&lt;p&gt;HSKs wie mein Yubikey können private Schlüssel für SecureShell-Zugriffe abspeichern. Somit muss man sie nicht mehr lokal auf jedem Endgerät ablegen (das bleibt aber konfigurierbar, siehe unten). Zudem sind sie gegenüber der Verwahrung auf einem Rechner besser gegen unbefugte Zugriffe geschützt.&lt;/p&gt;&#10;&lt;p&gt;Also probiere ich aus, meinen Github-SSH-Zugriff auf HSK umzustellen und folge &lt;a href="https://www.yubico.com/blog/github-now-supports-ssh-security-keys/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Yubicos Anleitung für passwordlose Authentisierung für Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Das Verfahren zur Erzeugung von SSH-Schlüsseln nach FIDO unterscheidet sich für verschiedene Schlüsselhersteller nicht. Ich verlinke ebenfalls die &lt;a href="https://docs.nitrokey.com/nitrokeys/features/fido2/ssh" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Nitrokey-Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; als Referenz.&lt;/p&gt;&#10;&lt;h2 id="was-so-nicht-funktioniert"&gt;Was so nicht funktioniert&lt;/h2&gt;&#10;&lt;p&gt;Doch mit den Empfehlungen von Yubico selbst komme ich auf meinem Betriebssystem und der Standardinstallation von &lt;code&gt;openssh &lt;/code&gt;nicht zum Ziel. Das aus der Anleitung zusammengebaute Kommando lautet:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# ssh-keygen -t ed25519-sk -O resident -O verify-required -O application&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ssh:github -O user&lt;span style="color:#f92672"&gt;=&lt;/span&gt;schallbert -C &lt;span style="color:#e6db74"&gt;&amp;#34;schallbert@github.com&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nach Erstellung muss man den öffentliche Schlüssel (Dateiendung &lt;code&gt;.pub&lt;/code&gt;) in der Web-Anwendung des jeweiligen Dienstes im Menüpunkt SSH-Zugang hinterlegen.&lt;/p&gt;&#10;&lt;h3 id="bedeutung-des-kommandos-und-der-wichtigsten-optionen"&gt;Bedeutung des Kommandos und der wichtigsten Optionen&lt;/h3&gt;&#10;&lt;p&gt;Dieser Befehl&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; sagt der Anwendung &lt;a href="https://www.man7.org/linux/man-pages/man1/ssh-keygen.1.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ssh-keygen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, dass sie mir einen Schlüssel des Typs &lt;code&gt;-t ed25519-sk&lt;/code&gt; erzeugen soll. Die Endung &lt;code&gt;-sk&lt;/code&gt; sagt aus, dass der Schlüssel auf einem Security Key - in meinem Fall dem Yubikey - gespeichert werden soll.&lt;/p&gt;&#10;&lt;p&gt;Um das Flag &lt;code&gt;-O resident&lt;/code&gt; zu verstehen muss man wissen, dass FIDO Authenticator-Schlüssel wie mit &lt;code&gt;-sk&lt;/code&gt; erzeugt aus zwei Teilen bestehen: Ein &lt;code&gt;key handle&lt;/code&gt; Teil, welcher auf der Festplatte des Rechners verbleibt und der private Schlüssel selbst, welcher &lt;a href="https://www.man7.org/linux/man-pages/man1/ssh-keygen.1.html#FIDO_AUTHENTICATOR" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nur auf dem HSK existiert&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Nur zusammen können sie Authentisierungsanfragen korrekt beantworten. Das Flag selbst weist jedoch an, beide Schlüsselteile auf den Yubikey zu schreiben. Das erleichtert die Verwendung mehrerer Geräte mit demselben Schlüssel: Der &lt;code&gt;key handle&lt;/code&gt;-Teil kann vom Yubikey ausgelesen und auf anderen Endgeräten abgelegt werden. Auf der anderen Seite jedoch genügt nun das Verlorengehen des Yubikey allein, um möglichen Angreifern SSH-Zugriff zu geben.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-f filename&lt;/code&gt; legt Namen für die Schlüsseldateien fest. Macht besonders dann Sinn, wenn man mehrere SSH-Verbindungen oder HSM verwalten muss. Ich habe mir eine Konvention erstellt, um nicht durcheinander zu kommen. Diese orientiert sich stark an der Schreibweise des Schlüssel-Auslesens von einem HSK (&lt;code&gt;ssh-keygen -K&lt;/code&gt;).&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-f &amp;lt;keyType&amp;gt;_&amp;lt;protocol&amp;gt;:&amp;lt;service&amp;gt;_&amp;lt;user&amp;gt;_&amp;lt;device&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Als konkretes Beispiel würde mein Dateiname hier wie folgt lauten:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-f ed25519-sk_ssh:github_schallbert_yubikey-main&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Dateinamen mit Windows&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-05-03T00:00:00Z"&gt;&#10; 03. Mai 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Obacht bei Nutzung von Windows: Dort ist der Doppelpunkt &lt;code&gt;:&lt;/code&gt; nicht als Zeichen in Dateinamen erlaubt. Er wird durch den Unterstrich &lt;code&gt;_&lt;/code&gt; ersetzt, was in der &lt;code&gt;config&lt;/code&gt; entsprechend abgebildet werden muss (&lt;code&gt;strg+h&lt;/code&gt; -&amp;gt; &lt;code&gt;:&lt;/code&gt; ersetzen mit &lt;code&gt;_&lt;/code&gt;)&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;p&gt;&lt;code&gt;-C schallbert@github.com&lt;/code&gt; fügt der Schlüsseldatei selbst einen Kommentar hinzu, zum Beispiel eine Kontaktadresse. Somit bekommt der Schlüssel ein Label, was die Handhabung unter mehreren vereinfacht.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-O application=ssh:github&lt;/code&gt; dient nur der sauberen Auflistung im Authenticator-Programm und sorgt dafür, dass ich Schlüssel nicht miteinander verwechseln kann. Ansonsten steht dort nämlich nur &lt;code&gt;ssh:keygen&lt;/code&gt; und ich kann nicht sehen, für welche Services ich Schlüssel hinterlegt habe. Außerdem hilfreich bei der Verwendung von &lt;code&gt;-resident&lt;/code&gt;, da bei der Extraktion der Schlüssel mit &lt;code&gt;ssh-keygen -K&lt;/code&gt; Unterscheidbarkeit gegeben bleibt.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-O user=schallbert&lt;/code&gt; hat für mich keinen praktischen Nutzen, da SSH dieses Flag nicht interessiert und der Yubikey Authenticator meinen Nutzernamen anders als erwartet nicht anzeigt.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-O no-touch-required&lt;/code&gt; vermeidet, dass der Hardware Security Key bei jeder Verwendung des Schlüssels berührt werden muss. Nützlich bei sehr häufiger Verwendung, allerdings Sicherheitsverlust durch Automatisierbarkeit.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-O verify-required&lt;/code&gt; sagt aus, dass die PIN des HSM abzufragen ist, bevor der Schlüssel genutzt werden kann. Eigentlich als zusätzliche Sicherheit gedacht, bekomme ich damit in meiner Shell jedoch Probleme. Und zwar wird die PIN einmal abgefragt, erzeugt aber vom &lt;code&gt;ssh-agent&lt;/code&gt; nicht jedes mal eine Abfrage in meiner Konsole. Sobald ich in einer anderen Session versuche mich per SSH zu verbinden, erhalte ich eine Fehlermeldung &lt;code&gt;agent refused operation&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# ssh -T git@github.com&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sign_and_send_pubkey: signing failed &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; ED25519-SK &lt;span style="color:#e6db74"&gt;&amp;#34;schallbert@github.com&amp;#34;&lt;/span&gt; from agent: agent refused operation&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@opnsense: Permission denied &lt;span style="color:#f92672"&gt;(&lt;/span&gt;publickey&lt;span style="color:#f92672"&gt;)&lt;/span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="woran-hat-es-gelegen"&gt;Woran hat es gelegen?&lt;/h2&gt;&#10;&lt;p&gt;Nach langer, frustrierender Fehlersuche (Was mache ich falsch? Funktioniert es mit dem Zweitschlüssel? Warum geht es beim ersten mal, danach aber nicht mehr? Was ist, wenn ich die Schlüssel erneut erstelle? Was passiert an anderen Rechnern? Was meint &amp;ldquo;KI&amp;rdquo; dazu? - Letzteres kostete bestimmt eine Stunde und ließ mich komplett auf der Stelle treten) probiere ich es mit einem anderen Kommando. Das funktioniert auf Anhieb.&lt;/p&gt;&#10;&lt;h2 id="was-hingegen-funktioniert"&gt;Was hingegen funktioniert&lt;/h2&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# ssh-keygen -t ed25519-sk -O application&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ssh:github -C &lt;span style="color:#e6db74"&gt;&amp;#34;schallbert@github.com&amp;#34;&lt;/span&gt; -f ed25519-sk_ssh:github_schallbert_yubikey-main&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Lasse ich &lt;code&gt;verify-required&lt;/code&gt; weg, kann ich mich erfolgreich anmelden und der SSH-agent läuft sauber durch.&lt;/p&gt;&#10;&lt;h3 id="was-verify-required-benötigt"&gt;Was &lt;code&gt;verify-required&lt;/code&gt; benötigt&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://superuser.com/questions/1804740/yubikey-ssh-authentication-fails-with-signing-failed-for-ecdsa-sk" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dieser Post im Forum superuser&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bringt für mein Fehlerbild Licht ins Dunkel. Die Terminal-Session mit der ich das SSH-Schlüsselpaar erstelle speichert die per &lt;code&gt;verify-required&lt;/code&gt; Flag erzwungene Pin-Eingabe zwischen. Daher funktioniert der Login erst einmal. Logge ich mich später oder in einem anderen Fenster ein, wie ich es oben getan habe um z.B. &lt;code&gt;git pull&lt;/code&gt; in einem meiner Repos zu prüfen, fragt der SSH-Agent meine PIN nicht ab.&lt;/p&gt;&#10;&lt;p&gt;Das liegt wohl daran, dass der SSH-Agent selbst keine Prompts an das anfragende Programm ausgeben kann. Abhilfe schafft ein Untermodul von Openssh: &lt;a href="https://packages.fedoraproject.org/pkgs/openssh/openssh-askpass/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;openssh-askpass&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Dieses kann man dem Agent übergeben, welcher dann die PIN-Abfrage über &lt;em&gt;askpass&lt;/em&gt; an den User übermittelt.&lt;/p&gt;&#10;&lt;h3 id="installation-von-ssh-askpass"&gt;Installation von ssh-askpass&lt;/h3&gt;&#10;&lt;p&gt;Mein Betriebssystem hatte &lt;em&gt;ssh-askpass&lt;/em&gt; nicht installiert, daher hole ich das hier nach. Es verwendet den Paketmanager &lt;code&gt;DNF&lt;/code&gt; &amp;ldquo;Dandified YUM&amp;rdquo; statt &lt;em&gt;apt&lt;/em&gt; oder anderer gängiger Manager. Dabei ist die Namensgebung meiner Meinung nach nicht 100% einheitlich, spreche ich den ssh-Programme doch im Terminal mit &lt;em&gt;ssh-agent&lt;/em&gt; an, muss jedoch &lt;em&gt;openssh-agent&lt;/em&gt; tippen für die Installation.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# verify-required needs openssh subpackage through Fedora&amp;#39;s package manager&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# sudo dnf install openssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Installing:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; openssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Complete!&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nun will ich dem Agent mitteilen, dass er &lt;em&gt;ssh-askpass&lt;/em&gt; zum Erfragen der PIN ansprechen kann. Dafür mus ich wissen, in welchem Pfad sich das Submodul befindet.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# which openssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/which: no openssh-askpass &lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Komisch, aber ich hatte es doch installiert! Frage ich mal den Paketmanager:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# dnf repoquery --list openssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/libexec/openssh/ssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Aha, openssh versteckt sich im Ordner &lt;code&gt;libexec&lt;/code&gt;. Na dann kann ich das Modul jetzt dem Agent bekanntgeben.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# eval &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;ssh-agent -s; SSH_ASKPASS&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/usr/libexec/openssh/ssh-askpass&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Agent pid &lt;span style="color:#ae81ff"&gt;16747&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-ssh-askpass-prompt.avif" alt="Image: Testing the ssh connection to Github with ssh -T and getting prompted for HSK device pin"&gt;&lt;/figure&gt;&#10;Ich prüfe die Verbindung zu Github und bekomme wie gewünscht einen Dialog angezeigt, der meine PIN abfragt. Anschließend muss ich den HSK noch berühren und bekomme dann die Erfolgsmeldung. Damit das alles auch in einem neuen Terminal und nach Neustart des Rechners funktioniert, füge ich eine Referenz auf den Schlüssel in meine Konfigurationsdatei ein.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ~/.ssh/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host github&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; User git&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Hostname github.com&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PreferredAuthentications publickey&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IdentityFile ~/.ssh/ed25519-sk_ssh:github_schallbert_yubikey-main&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Um auf Nummer sicher zu gehen, den Agenten neustarten und neue Terminalsessions erstellen. Sollten Zugangsdaten im Cache gewesen sein, ist das hiermit zurückgesetzt. Anschließend erneut die SSH-Verbindung testen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; schallbert@machine:~# eval &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;ssh-agent -k&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; eval &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;ssh-agent -s&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="nervig-gnome-popup-allow-inhibiting-shortcuts"&gt;Nervig: Gnome-Popup &amp;ldquo;Allow inhibiting shortcuts&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-gnome-inhibit-shortcuts-prompt.avif" alt="Image: Gnome popup *Allow inhibiting shortcuts*"&gt;&lt;/figure&gt;&#10;Einziger Wehrmutstropfen ist, dass ich jetzt beim Verbinden mit SSH ein zusätzliches Popup bekomme. Das wird durch die grafische Nutzeroberfläche &lt;em&gt;gnome&lt;/em&gt; ausgelöst. Leider finde ich keine Option, die zugehörige Einstellung permanent speichern. Somit wird es bei jeder Verbindung mit &lt;code&gt;verify-required&lt;/code&gt; angezeigt. Bei Gnome wird ist das Problem bereits &lt;a href="https://gitlab.gnome.org/GNOME/gnome-connections/-/work_items/85" target="_blank" rel="noopener noreferrer" class="external-link"&gt;für andere Anwendungen bekannt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, anscheinend aber noch nicht abgestellt.&lt;/p&gt;&#10;&lt;h2 id="übertragung-auf-windows-systeme"&gt;Übertragung auf Windows-Systeme&lt;/h2&gt;&#10;&lt;p&gt;Ich habe meine Schlüssel ja mit der Option &lt;code&gt;-O resident&lt;/code&gt; erzeugt. Da liegt es nahe, die Extraktion des &lt;code&gt;key-handle&lt;/code&gt; mal auf einem komplett anderen System zu testen: &lt;em&gt;Windows&lt;/em&gt;.&lt;/p&gt;&#10;&lt;p&gt;Ich öffne die Kommandozeile und tippe&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@windows-machine:~# ssh-keygen -K&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Enter PIN &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; authenticator:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;You may need to touch your authenticator to authorize key download.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Provider &lt;span style="color:#e6db74"&gt;&amp;#34;internal&amp;#34;&lt;/span&gt; returned failure -1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unable to load resident keys: invalid format&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Invalid format? Im Netz steht öfters geschrieben, dass die Schlüssel manchmal am Ende noch ein &lt;a href="https://stackoverflow.com/questions/47859437/windows-10-openssh-key-invalid-format#68593742" target="_blank" rel="noopener noreferrer" class="external-link"&gt;LineFeed-Zeichen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; benötigen um richtig erkannt zu werden. Ohne Download vom HSK kann ich sie aber nicht modifizieren. Also übertrage ich öffentlichen Schlüssel und key-handle manuell auf den Rechner und probiere testweise das &lt;code&gt;git pull&lt;/code&gt;-Kommando aus. Und siehe da: Es klappt!&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-windows-fido-key-ssh-pin-prompt.avif" alt="Image: FIDO ssh connection test on a Windows machine: OS prompts for PIN entry. It works!"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Möchte man also die Schlüssel mit verschiedenen Betriebssystemfamilien verwenden, nutzt die Option &lt;code&gt;-resident&lt;/code&gt; anscheinend nichts. Der Befehl &lt;code&gt;ssh-keygen -K&lt;/code&gt; ist nicht granular steuerbar: Er lädt alle auf dem HSK befindlichen resident keys herunter.&lt;/p&gt;&#10;&lt;h2 id="vps-vorsicht-bei-schlüssel-ersatz"&gt;VPS: Vorsicht bei Schlüssel-Ersatz&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-hetzner-key-add-irrelevant-for-existing-servers.avif" alt="Image: Hetzner telling me that new SSH keys will not propagate to existing resources (VPS). Important to know!"&gt;&lt;/figure&gt;&#10;Noch ein Hinweis aus eigener Erfahrung: Hat man bereits SSH-Schlüssel hinterlegt bei einem Dienstanbieter, der selber Ressourcen bereitstellt (In meinem Fall einen Server als VPS) so werden neue SSH-Schlüssel meist nicht über das Web-Frontend an diese Ressource weitergereicht.&lt;/p&gt;&#10;&lt;p&gt;Die &amp;ldquo;alten&amp;rdquo; Schlüssel werden folglich weiterhin benötigt und die Neuen nicht verwendet, obwohl sie in der Web-App abgelegt sind!&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Die neuen Schlüssel müssen dem Ziel manuell bekanntgemacht werden.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Das führe ich der &lt;a href="https://community.hetzner.com/tutorials/howto-ssh-key/de#schritt-4---ssh-key-auf-server-hinterlegen" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung von meinem Hoster Hetzner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; folgend mit dem Befehl &lt;code&gt;ssh-copy-id&lt;/code&gt; aus.&lt;/p&gt;&#10;&lt;h3 id="neue-schlüssel-manuell-auf-vps-ablegen"&gt;Neue Schlüssel manuell auf VPS ablegen&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# ssh-copy-id -i ./ed25519-sk_ssh:server_schallbert_yubikey-main.pub server &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dabei ist &lt;code&gt;server&lt;/code&gt; der SSH-Alias für meinen VPS. Ich verbinde mittels des bekannten, &amp;ldquo;alten&amp;rdquo; Schlüssels lasse den &amp;ldquo;Neuen&amp;rdquo; ablegen. Doch irgend etwas funktioniert nicht wie erwartet:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key&lt;span style="color:#f92672"&gt;(&lt;/span&gt;s&lt;span style="color:#f92672"&gt;)&lt;/span&gt;, to filter out any that are already installed&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/ssh-copy-id: WARNING: All keys were skipped because they already exist on the remote system.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;&#9;&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; you think this is a mistake, you may want to use -f option&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="schlüssel-erfolgreich-übertragen"&gt;Schlüssel erfolgreich übertragen?&lt;/h3&gt;&#10;&lt;p&gt;Ich logge mich also auf meinem Server ein und prüfe, ob der neue Schlüssel tatsächlich in der &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; Datei auftaucht. Tut er nicht. Also führe ich das obige Kommando erneut aus, diesmal mit aktivierter &lt;code&gt;-f (Force)&lt;/code&gt; Option. Und tatsächlich, die Übertragung klappt!&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# ssh-copy-id -f -i ./ed25519-sk_ssh:server_schallbert_yubikey-main.pub server &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Number of key&lt;span style="color:#f92672"&gt;(&lt;/span&gt;s&lt;span style="color:#f92672"&gt;)&lt;/span&gt; added: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Now try logging into the machine, with: &lt;span style="color:#e6db74"&gt;&amp;#34;ssh -i &amp;lt;identityFile&amp;gt; &amp;#39;server&amp;#39;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;and check to make sure that only the key&lt;span style="color:#f92672"&gt;(&lt;/span&gt;s&lt;span style="color:#f92672"&gt;)&lt;/span&gt; you wanted were added.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Zum Testen wie empfohlen nun mit dem neuen Schlüssel einloggen und&amp;hellip; Läuft! Erst jetzt können die alten Schlüssel sicher gelöscht werden: &lt;code&gt;shred -u &amp;lt;path/to/private/key&amp;gt;&lt;/code&gt;&lt;/p&gt;&#10;&lt;h3 id="auf-direktem-wege-ohne-ssh-copy-id"&gt;Auf direktem Wege ohne ssh-copy-id&lt;/h3&gt;&#10;&lt;p&gt;Will man den neuen Schlüssel ohne zusätzliche Werkzeuge bekannt machen, so kann man den Inhalt der öffentlichen Schlüsseldatei &lt;code&gt;&amp;lt;key-filename&amp;gt;.pub&lt;/code&gt; auch direkt selbst in eine neue Zeile von &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; schreiben. Dafür benötigt man lediglich eine offene SSH-Verbindung zum Ziel.&lt;/p&gt;&#10;&lt;h2 id="überlegungen-zu-komfort-und-bedienbarkeit"&gt;Überlegungen zu Komfort und Bedienbarkeit&lt;/h2&gt;&#10;&lt;p&gt;Wenn ich mir meine SSH-Verbindungen so ansehe, dann teilen sie sich grob in zwei Kategorien:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Admin-Zugänge&lt;/strong&gt; (VPS, NAS, Raspberry Pi) und Signaturen&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Protokoll-Zugänge&lt;/strong&gt; (Git etc.)&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Erstere erfordern aus meiner Sicht ein höheres Sicherheitsniveau. Zudem nutze ich sie nicht so häufig wie Versionsverwaltungen. Daher empfehle ich für die erste Kategorie &lt;code&gt;verify-required&lt;/code&gt; und würde dies bei der Zweiten weglassen. Mit der Option &lt;code&gt;no-touch-required&lt;/code&gt; würde ich ebenso verfahren: falls alle paar Minuten ein Commit hochgeladen werden soll, werden ständige Berührungen des Schlüssels schnell irritierend.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Alle Optionen bekommt man bei Aufruf des Handbuchs per &lt;code&gt;man ssh-keygen&lt;/code&gt; angezeigt.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Hardware Security Keys</title><link>https://blog.schallbert.de/hardware-security-keys-yubikey/</link><pubDate>Sat, 25 Apr 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/hardware-security-keys-yubikey/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-yubikey_thumb.avif"&#10; class="post-cover"&#10; alt="Image: A brand-new hardware security key in its package on a table. The image has been altered; its colors are value-inverted."&#10; title="Hardware Security Keys" /&gt;&#10;&lt;h2 id="begriffsabgrenzung-tpm-und-hsk"&gt;Begriffsabgrenzung: TPM und HSK&lt;/h2&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: center"&gt;Akronym&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: center"&gt;Bezeichnung&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: center"&gt;Kurzbeschreibung&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;TPM&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;Trusted Platform Module&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;Fest verbaut. Schützt das Gerät selbst&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;HSK&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;Hardware Security Key&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;Mobil. Schützt Konten, Zertifikate, private Schlüssel&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Das &lt;a href="https://de.wikipedia.org/wiki/Trusted_Platform_Module" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Trusted Platform Module&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist ein Sicherheitschip, der kryptografische Schlüssel sicher speichert und Funktionen wie Secure Boot, BitLocker-Verschlüsselung und Geräteidentität bereitstellt. Er ist fest auf der Hauptplatine eines Computers oder Smartphones verlötet.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-this-is-a-yubikey.avif" alt="Image: A Hardware Security Key with USB-C connector called YubiKey5 NFC."&gt;&lt;/figure&gt;&#10;Ein &lt;a href="https://en.wikipedia.org/wiki/Hardware_security_module" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hardware Security Key &lt;em&gt;HSK&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, Hardware-Sicherheitsschlüssel hingegen ist meist mobil und kann mit verschiedenen Geräten verbunden werden. Es kann Schlüssel nicht nur speichern, sondern meist auch kryptografische Operationen (Schlüsselgenerierung, Signatur, Verschlüsselung) ausführen. HSKs sind hochsicher und manipulationsgeschützt konstruiert. Sie können extern zertifiziert bzw. auditiert sein, sodass auch hochsensible Vorgänge wie Zahlungstransaktionen ausgeführt werden können.&lt;/p&gt;&#10;&lt;h2 id="was-bisher-geschah"&gt;Was bisher geschah&lt;/h2&gt;&#10;&lt;p&gt;Ich hörte einen Podcast vom Bundesamt für Sicherheit in der Informationstechnik (BSI) zum Thema &lt;a href="https://www.bsi.bund.de/SharedDocs/Audio/DE/BSI/Update_verfuegbar_Folge40_2024_02_29.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Passkey statt Passwort&amp;rdquo;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Den Empfehlungen folgend habe ich mir ein mobiles Hardware-Sicherheitsmodul (HSM) gekauft, um in Zukunft &lt;a href="https://en.wikipedia.org/wiki/WebAuthn" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Passkeys&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für meine Dienste zu verwenden. Schon lange bin ich nicht mehr glücklich mit meiner fragmentierten Geheimnisverwaltung aus Zetteln, Browser-Plugins, offline-Passwortmanagern wie &lt;a href="https://en.wikipedia.org/wiki/KeePass" target="_blank" rel="noopener noreferrer" class="external-link"&gt;KeePass&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, auf Festplatte abgelegten &lt;a href="https://en.wikipedia.org/wiki/Secure_Shell" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;SSH&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;-Schlüsseln, Backup-Keys und &lt;a href="https://askleo.com/how-should-i-keep-recovery-codes-secure/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Notfallcodes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; diverser Anbieter.&lt;/p&gt;&#10;&lt;p&gt;Ich möchte endlich mal Ordnung in die Landschaft bringen, ohne mich an einen kommerziellen Anbieter so fest zu binden, dass ich von ihm abhängig werde.&lt;/p&gt;&#10;&lt;h2 id="zielvorstellung"&gt;Zielvorstellung&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Keine Passwörter oder Login-Daten mehr merken müssen&lt;/li&gt;&#10;&lt;li&gt;Möglichst einfache, dabei sichere Verwaltung von Passwörtern aufbauen&lt;/li&gt;&#10;&lt;li&gt;Ordnung in den Accountwust bringen&lt;/li&gt;&#10;&lt;li&gt;Digitalen Nachlass regeln&lt;/li&gt;&#10;&lt;li&gt;Anderen ermöglichen, es mir gleichzutun&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Welche Erwartung ich jetzt schon einmal nehmen kann: Ganz ohne Passwörter oder Pins geht es auch mit dem HSK nicht. Ein Yubikey ist kein Passwortmanager. Er kann zwar Zertifikate, Passkeys und Secrets für OTPs halten, gibt diese Geheimnisse dann aber nicht mehr preis. Mit seiner Hilfe kann man signieren (Authentizität beweisen), Verschlüsseln und abgeleitete Schlüssel erstellen.&lt;/p&gt;&#10;&lt;p&gt;Vielmehr lässt sich die Passwortverwaltung mit Hilfe eines Hardware-Sicherheitsschlüssels aber zentralisieren und vereinfachen, da gängige Passwortmanager z.B. Passkeys unterstützen und so kein &amp;ldquo;Masterpasswort&amp;rdquo; mehr nötig ist.&lt;/p&gt;&#10;&lt;h3 id="yubikey"&gt;Yubikey&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-yubico-authenticator.avif" alt="Image: Screenshot of Yubico Authenticator&amp;#39;s menu options, showing `Home`, `Accounts`, `Passkeys`, `Certificates`, and `Slots`"&gt;&lt;/figure&gt;&#10;Als HSK nutze ich zwei &lt;a href="https://www.yubico.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Yubikey&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Schlüsselanhänger. Einen zum immer dabeihaben und einen als Zweitschlüssel, welcher an einem sicheren Ort verwahrt wird. Es gibt noch andere Anbieter mobiler Hardware Security Keys wie &lt;a href="https://www.nitrokey.com/de" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Nitrokey&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Die kannte ich vor Verfassen dieses Artikels noch nicht. Das ist bedauerlich, ist ihr Produkt doch quelloffen und damit idealer Testkandidat für mein Blog 😥.&lt;/p&gt;&#10;&lt;h3 id="yubico-authenticator"&gt;Yubico Authenticator&lt;/h3&gt;&#10;&lt;p&gt;Das GUI-Programm &amp;ldquo;Yubikey Authenticator&amp;rdquo; ist nützlich zum Einrichten, Belegen der Funktionstaste (z.B. Bestätigung oder Passwortausgabe als Tastatursimulation) und falls man &lt;a href="https://www.bsi.bund.de/DE/Themen/Verbraucherinnen-und-Verbraucher/Informationen-und-Empfehlungen/Cyber-Sicherheitsempfehlungen/Accountschutz/Zwei-Faktor-Authentisierung/zwei-faktor-authentisierung_node.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;2FA&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;-Einmalkennwörter z.B. als &lt;a href="https://en.wikipedia.org/wiki/Time-based_one-time_password" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;TOTP&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erzeugen will. Außerdem unterstützt mein Yubikey &lt;a href="https://en.wikipedia.org/wiki/Public_key_infrastructure" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;PKI&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, sodass man dort unter anderem auch &lt;a href="https://letsencrypt.org/certificates/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;SSL&lt;/em&gt;-Zertifikate&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und private Schlüssel geschützt ablegen kann.&lt;/p&gt;&#10;&lt;h2 id="konzept-des-hsk"&gt;Konzept des HSK&lt;/h2&gt;&#10;&lt;p&gt;Dabei gehört es zum Konzept, dass private Schlüssel entweder direkt auf dem Hardware-Schlüssel erzeugt werden oder - ohne sie lokal dauerhaft zu speichern - zwar auf einem Endgerät erstellt, aber auf dem Yubikey abgelegt werden. Diese verbleiben auf dem Schlüssel und sind nicht exportierbar. Er unterstützt gängige asymmetrische Verfahren wie &lt;em&gt;RSA&lt;/em&gt; und &lt;em&gt;ECDH&lt;/em&gt;. Für Details siehe z.B. &lt;a href="https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR02102/BSI-TR-02102.pdf?__blob=publicationFile&amp;amp;v=15" target="_blank" rel="noopener noreferrer" class="external-link"&gt;diese BSI-Publikation über kryptografische Verfahren&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Außerdem wird der Schlüssel die meiste Zeit offline, also ohne Energieversorgung herumliegen und dadurch kaum angreifbar sein. Eine zusätzliche Pin als Schutzmaßnahme bei Verlust des Schlüssels ist ein weiterer Sicherheitsfaktor.&lt;/p&gt;&#10;&lt;h3 id="zweitschlüssel"&gt;Zweitschlüssel&lt;/h3&gt;&#10;&lt;p&gt;Dennoch sollte man sich notieren, welche Zugänge man mit welchem Verfahren schützt, damit die richtigen Accounts im Falle des Verlustes gesperrt werden können. Ein &amp;ldquo;&lt;a href="https://support.yubico.com/s/article/How-to-register-your-spare-key" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Zweitschlüssel&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;rdquo; sollte vorhanden und belegt sein, was fast genauso zeitaufwendig ist wie den Hauptschlüssel zu konfigurieren.&lt;/p&gt;&#10;&lt;h3 id="nfc"&gt;NFC&lt;/h3&gt;&#10;&lt;p&gt;Von der &lt;a href="https://en.wikipedia.org/wiki/Near-field_communication" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;NFC&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;-Unterstützung meines Schlüssels hatte ich mir erhofft, Logins noch schneller und leichter hinter mich bringen zu können. Zwei Dinge hier stören mich jedoch: Meinen Laptop kriege ich partout nicht dazu, den Yubikey per NFC zu erkennen. Und auf dem Handy funktioniert die NFC-Erkennung höchstens mittelmäßig. Halte ich den Schlüssel dran, startet die App und bittet mich um Eingabe der Pin. Tue ich dies, muss ich den Schlüssel wieder entfernen und erneut an den Sensor halten &amp;ldquo;Schlüssel nicht bewegen!&amp;rdquo;. Das funktioniert längst nicht jedes mal und wirkt insgesamt ziemlich störanfällig. Schade.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-nfc-device-manager.avif" alt="Image: NFC shown in device manager of my Windows laptop: Drivers installed but not detecting my Yubikey"&gt;&lt;/figure&gt;&#10;&lt;h2 id="was-mir-beim-umgang-mit-yubikey-fehlt"&gt;Was mir beim Umgang mit Yubikey fehlt&lt;/h2&gt;&#10;&lt;p&gt;Insgesamt vermisse ich ein paar Komfort-Funktionen beim alltäglichen Umgang mit dem Yubikey.&#10;Bei Klick auf die gewünschten Schlüssel oder Accounts passiert nämlich nichts. Und konfigurieren kann ich da anscheinend auch nichts.&lt;/p&gt;&#10;&lt;p&gt;Bei der Verwendung von &lt;strong&gt;Passkeys&lt;/strong&gt; würde ich mir eine Verknüpfung wünschen. Ein Klick auf den entsprechenden Schlüssel im Authenticator sollte die entsprechende App bzw. das Browserfenster mit der Zielwebsite aufrufen. Da ich das HSK zu diesem Zeitpunkt bereits mit der PIN entsperrt habe, kann sogar der Anmeldevorgang nach Berührung des Schlüssels vollautomatisch stattfinden.&lt;/p&gt;&#10;&lt;p&gt;Ähnliches wünsche ich mir beim Umgang mit &lt;strong&gt;Accounts&lt;/strong&gt;. Warum heißt das Modul &amp;ldquo;Accounts&amp;rdquo;, wenn ich meine Konten nicht direkt darüber aufrufen kann? Auch hier muss ich die entsprechenden WebApps etc. selbst öffnen, den Passwortmanager am Start haben und dann noch die 2FA-Codes vom Authenticator herauskopieren. Das geht bestimmt einfacher.&lt;/p&gt;&#10;&lt;h3 id="touch-password-pin-puk-management-key-passphrase"&gt;Touch, password, PIN, PUK, management key, passphrase&lt;/h3&gt;&#10;&lt;p&gt;Je nach Modul gibt es auch bei der Verwendung des Yubikey zumeist eine Mehr-Faktor-Authentisierung. Das unkomplizierteste Verfahren ist eine Anwesenheitserkennung über eine Berührung des Sticks. Damit kann sichergestellt werden, dass eine Person das HSK aktiv bedient und nicht etwa automatisch eingeloggt werden kann. &lt;em&gt;Touch&lt;/em&gt; kann modulübergreifend angewendet werden; zur Detailkonfiguration komme ich &lt;a href="https://blog.schallbert.de/hardware-security-key-add-ssh/"&gt;in meinem Artikel über SSH&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Das &lt;em&gt;Accounts&lt;/em&gt;-Modul kann optional mittels Passwort, das &lt;em&gt;Passkeys&lt;/em&gt;-Modul per &lt;em&gt;PIN&lt;/em&gt; gesichert werden. Manche Anbieter verlangen die Einrichtung einer PIN als zweiten Faktor sogar.&lt;/p&gt;&#10;&lt;p&gt;Das &lt;em&gt;Certificates&lt;/em&gt;-Modul muss mit &lt;a href="https://docs.yubico.com/yesdk/users-manual/application-piv/pin-puk-mgmt-key.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PIN, und PUK und Management Key&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; vor unbefugtem Zugriff gesperrt werden. Private Schlüssel oder Zertifikate können optional zusätzlich mit einer &lt;a href="https://www.ssh.com/academy/ssh/passphrase" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Passphrase&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ausgestattet werden, was die Schlüssel selbst schützt (und aus meiner Sicht für den Yubikey keinen Sinn macht, da die privaten Schlüssel ohnehin nicht auslesbar sind).&lt;/p&gt;&#10;&lt;h2 id="funktionsumfang-und-kryptographische-landschaft"&gt;Funktionsumfang und kryptographische Landschaft&lt;/h2&gt;&#10;&lt;p&gt;Da das HSK einen so großen Sicherheitsgewinn verspricht, möchte ich natürlich möglichst viele seiner Fähigkeiten nutzen. Ich habe jetzt ein paar Wochen mit den verschiedenen Modulen herumgespielt und muss feststellen, dass man sich ganz schön hineindenken und viel lernen muss, um alle gut nutzen zu können. Immerhin versteht man danach ungefähr was die Module tun und bekommt einen guten Überblick zu den Themen Internet-Sicherheit, Verschlüsselungsverfahren und -Protokolle.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Accounts&amp;rdquo; und &amp;ldquo;Passkeys&amp;rdquo; sind für Eingabemasken (Plattformen, Webseiten, Shops) und Apps (Mailer, Passwortmanager, Messenger) einfach einzurichten: Absolut empfehlenswert! - schallbert&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Yubikey-Modul&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Beschreibung&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Accounts&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Verwaltung von Konten/Anmeldungen, unterstützt TOTP&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Passkeys&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Passwortloses Anmeldeverfahren durch &lt;a href="https://bitmi.de/it-sicherheitsblog-was-ist-eigentlich-fido2/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;FIDO/WebAuthn&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (Public-Key-Credential)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Certificates&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Speicherung und Nutzung von Zertifikaten; unterstützt &lt;a href="https://www.openpgp.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;OpenPGP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (PGP) und &lt;a href="https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR02103/BSI-TR-02103.pdf?__blob=publicationFile&amp;amp;v=2" target="_blank" rel="noopener noreferrer" class="external-link"&gt;X.509&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (PIV)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Slots&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Zwei programmierbare Slots zur Ausgabe statischer Passworte oder Einmalkennwörter&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Kompliziert und aufwendig wird es meiner Meinung nach mit Public/Private Keys und Zertifikaten. Erzeugungs- und Validierungsverfahren hängen vom Betriebssystem oder darauf verfügbaren Programmen ab und Anleitungen greifen teils zu kurz oder widersprechen sich: Nur für Nerds oder Profis. - schallbert&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Gehen wir die Module mal durch.&lt;/p&gt;&#10;&lt;h3 id="2-faktor-authentisierung-yubikey-accounts"&gt;2-Faktor-Authentisierung &amp;ldquo;Yubikey: Accounts&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Der Yubikey kann als Generator für zeitbasierte Einmalkennwörter dienen. Dafür legt man wie in jeder anderen Authenticator-App auch pro Dienst ein Konto an und übergibt einen Sicherheitsschlüssel (Geheimnis, Secret) an den Yubikey. Auf Basis dieses Geheimnisses können bei angestecktem HSK nun Einmalcodes erzeugt werden.&lt;/p&gt;&#10;&lt;p&gt;Da es keine Möglichkeit gibt, diese &amp;ldquo;Secrets&amp;rdquo; vom Yubikey zu exportieren, muss der Vorgang für Zweitschlüssel wiederholt werden. Doch nicht jeder Anbieter sieht die Nutzung mehrerer Authentikatoren vor. Also kann es sinnvoll sein, das Geheimnis in der Zwischenablage&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; zu halten und ihn bei der Konfiguration des/der Zweitschlüssel wiederzuverwenden.&lt;/p&gt;&#10;&lt;h3 id="passkeys-yubikey-passkeys"&gt;Passkeys &amp;ldquo;Yubikey: Passkeys&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Das Passkey-Modul meines Yubikey funktioniert recht gut. Viele Services&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; unterstützen die passwortlose Anmeldung leider noch nicht oder akzeptieren Passkeys nur als zweiten Faktor. Bei anderen Services&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt; kann ich das Passwort nicht deaktivieren, sodass ich kaum einen Sicherheitsgewinn sehe oder mir werden Backup-Codes ausgegeben, die im Notfall als zweiter Faktor dienen sollen&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;. Immerhin, ich kann durch den Passkey jetzt bei einigen Diensten die Passwort-TOTP-Anmeldung umgehen und bin schneller eingeloggt.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Die wohl größten Vorteile der Technologie für Nutzerinnen und Nutzer liegen darin, dass Sie in Zukunft zum einen keine Passwörter mehr erstellen und verwalten müssen. Zum anderen sind Passkeys immun gegen die breite Masse bekannter Phishing-Angriffe.&amp;rdquo; - &lt;a href="https://www.bsi.bund.de/DE/Themen/Verbraucherinnen-und-Verbraucher/Informationen-und-Empfehlungen/Cyber-Sicherheitsempfehlungen/Accountschutz/Passkeys/passkeys-anmelden-ohne-passwort_node.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;BSI-Website, Oktober 2025&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Die Authenticator-App braucht man nicht zur Konfiguration von Passkeys. Dies läuft komplett im Browser oder der Kommandokonsole ab. Beide müssen das Handling von Passkeys unterstützen.&lt;/p&gt;&#10;&lt;h3 id="keys--certificates-yubikey-certificates"&gt;Keys &amp;amp; Certificates &amp;ldquo;Yubikey: Certificates&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Um hier etwas Licht hineinzubringen, müssen wir uns erst einmal den Unterschied zwischen Schlüsselpaar und Zertifikat im Kontext der Authentisierung vor Augen führen. In beiden Fallen geht es darum, die Identität des Client zu verifizieren. Vor einem Server (z.B. per SSH), einer Institution (z.B. einer Certificate Authority) oder einem anderen Client (z.B. per &lt;em&gt;PGP&lt;/em&gt;). Somit wird für das Ziel feststellbar, dass es sich tatsächlich um den zur Verbindung berechtigten Client handelt.&lt;/p&gt;&#10;&lt;h3 id="publicprivate-key-pair"&gt;Public/Private Key Pair&lt;/h3&gt;&#10;&lt;p&gt;Das reine Schlüsselpaar (also kein Zertifikat) wird üblicherweise dann verwendet, wenn das Ziel der Kommunikation im Besitz desjenigen ist, der auch über den Client verfügt. Dann nämlich kann der Client zum Aufsetzen der Verbindung den öffentlichen Schlüssel auf dem Host platzieren. Dieses Verfahren wird z.B. bei Einrichten von SSH-Verbindungen und für einige APIs wie bei der Search Console verschiedener Suchmaschinenanbieter angewendet.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-gitea-ssh-keys.avif" alt="Image: Gitea account security settings: SSH public key stored in Gitea Webapp"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Der Host kann dem Client mit dem öffentlichen Schlüssel verschlüsselte Testnachricht schicken, welche nur mit dem zugehörigen privaten Schlüssel wieder in Klartext umgewandelt werden kann. Schickt der Client den korrekten Klartext zum Host zurück, ist für den Host bewiesen, dass der Verbindungspartner authentisch ist.&lt;/p&gt;&#10;&lt;h3 id="certificates--piv"&gt;Certificates / PIV&lt;/h3&gt;&#10;&lt;p&gt;Doch was, wenn der Host nicht unter der Kontrolle des Clients steht? Wie soll dann sichergestellt werden, dass sich weder Client noch Server für jemand anders ausgibt und einen gefälschten öffentlichen Schlüssel hinterlegt? Genau da kommen Zertifikate zum Einsatz.&lt;/p&gt;&#10;&lt;p&gt;Vereinfacht gesagt enthält das Zertifikat nicht nur den öffentlichen Schlüsselteil, sondern zusätzliche Informationen über den Netzteilnehmer und die gewünschte Verbindung. Nun kommt eine vertrauenswürdige, an der Kommunikation zwischen Client und Server unbeteiligte dritte Partei ins Spiel. In der Private Key Infrastructure &lt;em&gt;PKI&lt;/em&gt; nennt man sie Certificate Authority &lt;em&gt;CA&lt;/em&gt;.&lt;/p&gt;&#10;&lt;p&gt;Der Netzteilnehmer sendet nun den öffentlichen Schlüssel samt Metadaten über sich selbst an die CA und stellt einen sogenannten &lt;em&gt;CSR&lt;/em&gt;, einen Certificate Signing Request. Die CA prüft nun, ob der Kanal des Teilnehmers auch wirklich diesem gehört.&lt;/p&gt;&#10;&lt;p&gt;Bei einer E-Mail (Stichwort &lt;a href="https://www.gpg4win.org/doc/en/gpg4win-compendium_8.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PGP/GPG&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) kann die CA beispielsweise einen aus dem Public Key errechneten Hash-Link an die angegebene Mailadresse versenden. Wird der Link geklickt, beweist dies der CA gegenüber, dass die Mailadresse tatsächlich dem Client gehört und sie kann das Zertifikat ausstellen, den CSR also mit ihrem privaten Schlüssel signieren.&lt;/p&gt;&#10;&lt;p&gt;Bei Domains (Stichwort SSL/TLS) kann die CA beim Domain Name Service &lt;em&gt;DNS&lt;/em&gt;-Anbieter prüfen, ob die Angaben zur Anfrage des Servers passen.&lt;/p&gt;&#10;&lt;p&gt;Clients haben die Signaturen gängiger Certificate Authorities eingespeichert (z.B. im Browser). Wird das Zertifikat auf dem Rückweg zwischen Server und Client manipuliert, verändert sich der Hashwert und der Client spuckt eine Zertifikatswarnung aus: &amp;ldquo;invalid certificate&amp;rdquo;.&#10;Jetzt muss man jedoch den Certificate Authorities vertrauen&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt; 🙃&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Der Rechner sollte dafür offline sein. Wenn man supersicher sein will, macht man das Anlernen auf einem Live-Betriebssystem. Das bootet üblicherweise von einem USB-Stick und hat nach dem Herunterfahren alles vergessen.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;Z.B. Gar nicht: Mailbox, 2FA: Discord und Kraken, zum Zeitpunkt Apr-26. Vielleicht habe ich aber auch nicht die richtigen Einstellungen gefunden?&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;Z.B. Google, Github und Hetzner, zum Zeitpunkt Apr-26.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:4"&gt;&#10;&lt;p&gt;Auch diese Backup-Codes müssen sicher abgelegt werden. Idealerweise wieder offline, unbedingt aber unabhängig vom HSK.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:5"&gt;&#10;&lt;p&gt;Es gibt Alternativen zu Institutionen wie CAs, siehe &lt;a href="https://de.wikipedia.org/wiki/Web_of_Trust" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Web of Trust&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. So kann die Verifikation der Identität beispielsweise auch peer-to-peer von unbeteiligten Dritten stattfinden.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Giteas act_runner rootless: Startprobleme</title><link>https://blog.schallbert.de/act-runner-dind-failed-to-start-the-child/</link><pubDate>Sat, 28 Mar 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/act-runner-dind-failed-to-start-the-child/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-28-act_runner-dind-failed-to-start-child-solved-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Top page crop of OWASP&amp;#39;&amp;#39;s Docker Security Cheat Sheet [Source, downloaded May-26](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html) [License](https://creativecommons.org/licenses/by-sa/4.0/)"&#10; title="Giteas act_runner rootless: Startprobleme" /&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Gitea schickt `act_runner` in Rente&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-09-15T00:00:00Z"&gt;&#10; 15. Sept. 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Dieser Artikel bezieht sich auf eine Actions-Implementierung von Gitea, den &lt;code&gt;act_runner&lt;/code&gt;. Jener ist abgeleitet von &lt;a href="https://github.com/nektos/act" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nectos/act&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Gitea verwendet inzwischen &lt;a href="https://blog.gitea.com/release-of-runner-1.0.0/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;einen eigenen Runner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Der alte Runner sollte ausgetauscht werden. Weitere Infos: Blogartikel &lt;a href="https://blog.schallbert.de/build-deploy-hugo-with-actions-docker-caddy/"&gt;Hugo-Seite mit Gitea Actions, docker, und caddy bereitstellen&lt;/a&gt;&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="das-fehlerbild"&gt;Das Fehlerbild&lt;/h2&gt;&#10;&lt;p&gt;Mein &lt;a href="https://blog.schallbert.de/gitea-act-runner-dind/"&gt;docker-in-docker (DinD) act_runner&lt;/a&gt; stürzt kurz nach dem Start mit folgender Fehlermeldung ab:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-28-act_runner-dind-failed-to-start-child-problem.avif" alt="Image: console window with docker logs text output: `\[rootlesskit:parent\] error: failed to start the child: fork/exec /proc/self/exe: operation not permitted`"&gt;&lt;/figure&gt;&#10;&lt;h2 id="issue-ticket-bei-gitea"&gt;Issue-Ticket bei Gitea&lt;/h2&gt;&#10;&lt;p&gt;In der &lt;a href="https://gitea.com/gitea/act_runner/issues/721" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea-Community (issue #721)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; habe ich ein Ticket für das Problem angelegt. Im Folgenden gehe ich auf eigene Erkenntnisse ein und fasse die Diskussion etwas zusammen.&lt;/p&gt;&#10;&lt;h2 id="problem-mit-kernel-berechtigungen"&gt;Problem mit Kernel-Berechtigungen?&lt;/h2&gt;&#10;&lt;p&gt;Der &lt;code&gt;act_runner&lt;/code&gt; muss bei der Docker-in-Docker Konfiguration selbst einen eigenen &lt;a href="https://docs.docker.com/engine/security/#docker-daemon-attack-surface" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker Daemon&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; betreiben. Nur so kann der Runner für die &lt;em&gt;Actions&lt;/em&gt; eigene Container erstellen. Dies ist aus Sicherheitsgründen erst einmal nicht erlaubt.&lt;/p&gt;&#10;&lt;h3 id="warum-container-keine-container-starten-dürfen"&gt;Warum Container keine Container starten dürfen&lt;/h3&gt;&#10;&lt;p&gt;Im Kontext des &lt;em&gt;act_runner&lt;/em&gt; führt eine Action Code aus, der selbst Teil des Repository ist. Wird unbemerkt Schadcode in die Action eingeschleust, z.B. von einem &amp;ldquo;Collaborator&amp;rdquo;, so kann dieser bei einer Docker-Installation ohne DinD über den &lt;a href="https://docs.docker.com/engine/security/rootless/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;unter Umständen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; mit &lt;strong&gt;root-Rechten&lt;/strong&gt; ausgestatteten Docker-Daemon &lt;code&gt;dockerd&lt;/code&gt; direkt auf das Hostsystem durchgreifen.&lt;/p&gt;&#10;&lt;h3 id="auswirkungen-falls-trotz-dind-etwas-schief-geht"&gt;Auswirkungen, falls trotz DinD etwas schief geht&lt;/h3&gt;&#10;&lt;p&gt;Bei Anwendung des DinD-Konzeptes kann dies bei einem Angriff lediglich zum Zugriff auf den &lt;code&gt;dockerd&lt;/code&gt; innerhalb des &lt;em&gt;act_runner&lt;/em&gt; Containers führen - und das nur, wenn die Action nicht sauber gekapselt ist. Immer noch nicht gut, aber vertretbar: Bei Neustart des Containers ist der Status quo ante wiederhergestellt. Durchgriff auf das Hostsystem besteht allenfalls indirekt dadurch, dass der Container selbst mit sämtlichen Kernel-Features ausgestattet ist.&lt;/p&gt;&#10;&lt;h3 id="einfache-lösung-dind-container-privileged-starten"&gt;Einfache Lösung: DinD-Container &lt;code&gt;privileged&lt;/code&gt; starten&lt;/h3&gt;&#10;&lt;p&gt;Um dem DinD-Container jetzt die Möglichkeit zu geben eigene &lt;em&gt;Actions&lt;/em&gt; aufzusetzen, kann in der Konfiguration &lt;code&gt;privileged: true&lt;/code&gt; gesetzt werden. Damit werden dem Container &lt;strong&gt;alle Kernel-Fähigkeiten&lt;/strong&gt; eingeräumt. Dies ist laut (dem hervorragend gemachten) &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-3-limit-capabilities-grant-only-specific-capabilities-needed-by-a-container" target="_blank" rel="noopener noreferrer" class="external-link"&gt;OWASP Security Cheat Sheet&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; möglichst zu vermeiden. Sollte &lt;em&gt;act_runner&lt;/em&gt; nun selbst angegriffen werden oder kritische Sicherheitslücken offenbaren, hätte der Eindringling auf diese Weise bereits alle Möglichkeiten, der Kapselung vor dem Host-System zu entgehen.&lt;/p&gt;&#10;&lt;p&gt;Will man den einfachen Weg gehen, sieht die entsprechende &lt;code&gt;docker-compose.yml&lt;/code&gt; wie folgt aus.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# section ACT_RUNNER&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;runner&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/act_runner:latest-dind-rootless&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea-runner&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;privileged&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;And remember: Do not run containers with the &amp;ndash;privileged flag!!!&amp;rdquo; - OWASP&amp;rsquo;s Docker Security Cheat Sheet, 2026 &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;License&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="sackgasse-nur-unbedingt-benötigte-rechte-vergeben"&gt;Sackgasse: Nur unbedingt benötigte Rechte vergeben&lt;/h3&gt;&#10;&lt;p&gt;Nach kurzer Recherche finde ich einen &lt;a href="https://www.codestudy.net/blog/can-i-run-docker-in-docker-without-using-the-privileged-flag/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Artikel auf CodeStudy.net&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;, welcher sich mit dem Thema DinD beschäftigt. Hieraus bastele ich Änderungen in meiner &lt;code&gt;docker-compose.yml&lt;/code&gt; zusammen.&lt;/p&gt;&#10;&lt;p&gt;Meine Vorgehensweise:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Änderungen in &lt;code&gt;docker-compose.yml&lt;/code&gt; vornehmen&lt;/li&gt;&#10;&lt;li&gt;Service neu starten &lt;code&gt;docker compose restart &amp;lt;service&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Logs anschauen &lt;code&gt;docker logs --tail 100 &amp;lt;container-name&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Falls gestartet: Programm laufen lassen &lt;code&gt;web-app-&amp;gt;repo-&amp;gt;jobs-&amp;gt;rerun_all-jobs&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Bei Fehlschlag: Wiederholen&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Ich beginne mit wenigen, aber sehr mächtigen Rechten. Nach ein paar Iterationen erhalte ich:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# section act_runner DinD-rootless&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;runner&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/act_runner:latest-dind-rootless&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea-runner&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;privileged&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;cap_add&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;SYS_ADMIN&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;security_opt&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#66d9ef"&gt;no&lt;/span&gt;-&lt;span style="color:#ae81ff"&gt;new-privileges:true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;apparmor:unconfined &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;systempaths=unconfined&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;writable-cgroups=true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Aber noch immer fehlen mir Rechte, die außerhalb von Kernel-Privilegien und Sicherheitszonen liegen: Der Docker-Daemon möchte auf &lt;code&gt;sysfs&lt;/code&gt; und &lt;code&gt;proc&lt;/code&gt;, also Systemdateiverzeichnisse und -Prozesse zugreifen können.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;warning msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;[rootlesskit:child ] failed to mount sysfs, falling back to read-only mount: operation not permitted&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das kann ich selbst mit &lt;code&gt;cap_add: ALL&lt;/code&gt; nicht ausgleichen. Zudem wird die Quellenlage im Internet auf dieser technischen Ebene echt dünn (und ich bin kein Docker-Spezialist). Ein &lt;a href="https://zhsj.me/blog/view/dind-without-privileged" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Re-Mount&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; der Dateisysteme könnte hier noch helfen; das erscheint mir aber zu experimentell und fehleranfällig.&lt;/p&gt;&#10;&lt;p&gt;Nach weiteren Stunden Recherche und Trial-and-Error auf meinem Server muss ich &lt;a href="https://github.com/docker-library/docker/issues/546" target="_blank" rel="noopener noreferrer" class="external-link"&gt;@tianon in einer Diskussion auf Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zustimmen: Der Docker Daemon benötigt so viele Rechte und Fähigkeiten, dass man gleich bei &lt;code&gt;privileged: true&lt;/code&gt; bleiben kann und sich nicht mit einer Armada an &lt;code&gt;CAP_ADD&lt;/code&gt; und System-bind-mounts herumschlagen muss.&lt;/p&gt;&#10;&lt;h3 id="zukünftige-lösung-virtualisierung-oder-daemonless"&gt;Zukünftige Lösung: Virtualisierung oder Daemonless&lt;/h3&gt;&#10;&lt;p&gt;Doch es gibt möglicherweise bessere Lösungen: Eigene Container-Laufzeitumgebungen wie &lt;a href="https://github.com/nestybox/sysbox" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Sysbox&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Die Box hat selbst auf dem Hostsystem keine besonderen Rechte, ähnlich wie in einer virtuellen Maschine scheint sie in ihr laufenden Applikationen aber alle Rechte und Fähigkeiten zur Verfügung stellen zu können.&lt;/p&gt;&#10;&lt;p&gt;Installation und Inbetriebnahme bis hin zu funktionierenden Job-Containern führt für diesen Post zu weit. Daher muss ich vorerst auf andere &lt;a href="https://www.jaburjak.cz/posts/docker-in-docker-unprivileged/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Quellen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; verweisen.&lt;/p&gt;&#10;&lt;p&gt;Auch das Umschwenken &lt;a href="https://tiendu.github.io/2025/04/18/dind.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;von Docker auf Podman&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist eine mögliche Lösung: Podman ist Daemonless, rootless und bietet ähnlichen Funktionsumfang wie Docker. Ein Umzug nach Podman wäre für mich ein eigenes Projekt und passt nicht in den Kontext dieses Posts.&lt;/p&gt;&#10;&lt;h2 id="linux-security-modules-seccomp-apparmor-selinux"&gt;Linux Security Modules (seccomp, AppArmor, SELinux)&lt;/h2&gt;&#10;&lt;p&gt;Begriffsklärung: &lt;a href="https://www.kernel.org/doc/html/latest/admin-guide/LSM/index.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;LSM&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ermöglichen verschiedene Sicherheitsprüfungen und -Sperren auf Kernelebene. Durch &amp;ldquo;Mandatory Access Control&amp;rdquo; können Sicherheitserweiterungen wie AppArmor Kernel-Fähigkeiten für einzelne Anwendungen steuern und Zugriffe falls nötig sperren.&lt;/p&gt;&#10;&lt;h3 id="apparmor"&gt;Apparmor&lt;/h3&gt;&#10;&lt;p&gt;Ich betreibe meinen Server (VPS) mit Ubuntu. Dieses Betriebssystem hat in den aktuelleren Versionen &amp;ldquo;user namespace creation restrictions&amp;rdquo; in AppArmor integriert, was das Betreiben von &lt;em&gt;appimages&lt;/em&gt;, &lt;em&gt;WebApps&lt;/em&gt; und eben auch &lt;em&gt;Containers&lt;/em&gt; mit höheren Rechten verhindert.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Unprivileged user namespaces are a feature in the Linux kernel [&amp;hellip;]; it enables unprivileged users to gain administrator (root) permissions within a confined environment [&amp;hellip;]&amp;rdquo; - mbelair, Ubuntu Discourse, as of May-2026, &lt;a href="https://discourse.ubuntu.com/t/understanding-apparmor-user-namespace-restriction/58007" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Ubuntu Discourse Website&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Kurzgesagt wurde dieses Werkzeug als &lt;a href="https://de.wikipedia.org/wiki/H%C3%A4rten_%28Computer%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;security-hardening-Maßnahme&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; entwickelt, um Angriffsfläche auf dem Host-System bei Ausführung von Programmen zu reduzieren, die höhere Rechte erfordern.&lt;/p&gt;&#10;&lt;h3 id="einfache-lösung-komplett-abstellen"&gt;Einfache Lösung: Komplett abstellen&lt;/h3&gt;&#10;&lt;p&gt;Die Dampfhammermethode stellt das Feature für Benutzernamensräume komplett kalt. Wir teilen AppAmor mit, dass Drittprogramme wie in alten Betriebssystemversionen auch ohne Einschränkungen Kernel-Features oder höhere Rechte nutzen dürfen. Den zugehörigen Befehl habe ich aus dem &lt;a href="https://askubuntu.com/questions/1511854/how-to-permanently-disable-ubuntus-new-apparmor-user-namespace-creation-restric" target="_blank" rel="noopener noreferrer" class="external-link"&gt;askubuntu-Forum&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; entliehen.&lt;/p&gt;&#10;&lt;p&gt;Er schaltet die Einschränkung von Nicht-Admin-Benutzernamensräumen (die Übertragung ins Deutsche tut schon fast weh) für diese Sitzung ab, indem er die Kernelparameter zur Laufzeit umschreibt (&lt;code&gt;-w&lt;/code&gt;).&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbertTestsThis@machine:~# sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich halte den Befehl zur Fehlersuche für sehr hilfreich. Möchte man herausfinden, ob das gewünschte Programm wegen AppArmor nicht startet:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Obiges Kommando eingeben&lt;/li&gt;&#10;&lt;li&gt;das Drittprogramm, den Container etc. testen&lt;/li&gt;&#10;&lt;li&gt;durchbooten. Oder den Befehl mit &lt;code&gt;=1&lt;/code&gt; eingeben. Danach ist der Ursprungszustand wiederhergestellt.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Möchte man diese Verwundbarkeit permanent erhalten, so gibt man ein:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;makeVulnerability@machine:~# echo &lt;span style="color:#e6db74"&gt;&amp;#39;kernel.apparmor_restrict_unprivileged_userns = 0&amp;#39;&lt;/span&gt; | sudo tee /etc/sysctl.d/20-apparmor-donotrestrict.conf&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;makeVulnerability@machine:~# sudo shutdown -r now&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dies alleine stellt noch keine Sicherheitslücke dar. Es ist jetzt nur prinzipiell einfacher geworden, eventuelle Schwächen im Kernel auszunutzen und der &amp;ldquo;Sandbox&amp;rdquo; des Containers zu entrinnen.&lt;/p&gt;&#10;&lt;h3 id="richtige-lösung-für-jede-anwendung-justieren"&gt;Richtige Lösung: Für jede Anwendung justieren&lt;/h3&gt;&#10;&lt;p&gt;Es gibt Anwendungen (wie meine DinD-Version von &lt;code&gt;act_runner&lt;/code&gt;), die benötigen zwingend erweiterte Rechte um ordnungsgemäß zu funktionieren. Und nur diese sollten die Möglichkeit erhalten, Nicht-Admin-Benutzernamensräume zu erhalten.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://docs.docker.com/engine/security/apparmor/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker selbst&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; hat &lt;em&gt;AppAmor&lt;/em&gt; einen Abschnitt in der Dokumentation gewidmet. Um zur Lösung für den DinD-Runner zu gelangen, benötigt es eine &lt;a href="https://www.spad.uk/posts/rootless-dind-noble/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Transferleistung (danke, @thespad)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Zur Wiederholung nochmal die Fehlermeldung von oben:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;rootlesskit:parent&lt;span style="color:#f92672"&gt;]&lt;/span&gt; error: failed to start the child: fork/exec /proc/self/exe: operation not permitted&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner | s6-svwait: fatal: some services reported permanent failure or their supervisor died&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das Logfile sagt aus, dass der &lt;code&gt;parent&lt;/code&gt; (Docker-Daemon im Container) sein &lt;code&gt;child&lt;/code&gt; (Runner-Container) nicht starten kann, weil er für andere Teilnehmer (&lt;code&gt;fork&lt;/code&gt;) selbst keine Prozesse (&lt;code&gt;/proc/self&lt;/code&gt;) erstellen kann. Wer ist der User dieses Daemons? &lt;code&gt;rootlesskit&lt;/code&gt;.&#10;Genau hier hakt unsere Lösung ein: Wir müssen in der &lt;code&gt;docker-compose.yml&lt;/code&gt; das AppArmor-Profil für &lt;em&gt;rootlesskit&lt;/em&gt; im DinD-Container des act_runner freigeben.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# section act_runner DinD-rootless&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;runner&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/act_runner:latest-dind-rootless&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea-runner&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;privileged&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;security_opt&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;apparmor=rootlesskit&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Entsprechend gut sieht jetzt auch das Logfile aus. Fertig!&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-28-act_runner-dind-failed-to-start-child-solved.avif" alt="Image: act_runner rootless-DinD logfile with --privileged:true and --security_opt:apparmor=rootlesskit, showing a clean startup"&gt;&lt;/figure&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Ich habe das Gefühl, dass jener Post mit &amp;ldquo;KI&amp;rdquo; geschrieben wurde. Wenn es konkret wird und es um &amp;ldquo;critical volume mounts&amp;rdquo; geht, stehen dort nämlich keine echten System-Pfade mehr und der Artikel wird insgesamt so vage, dass ich den Ratschlägen folgend gar nicht bis zum Ziel komme.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Vortrag: Software-defined vehicle</title><link>https://blog.schallbert.de/announcements/2026-01-25-lecture-sdv-available/</link><pubDate>Sun, 25 Jan 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/announcements/2026-01-25-lecture-sdv-available/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/announcements/2026-01-25-SDV-thumb.avif"&#10; class="post-cover"&#10; alt="Image: text reading SDV - software defined vehicle"&#10; title="Vortrag: Software-defined vehicle" /&gt;&#10;&lt;h2 id="-dezember-2025--neuer-vortrag-veröffentlicht"&gt;&amp;mdash; Dezember 2025 &amp;mdash; Neuer Vortrag veröffentlicht&lt;/h2&gt;&#10;&lt;p&gt;Ich habe vor kurzem einen Vortrag zum Thema software-defined vehicle gehalten. Die Folien sind jetzt zur Ansicht auf meiner Hauptseite unter &lt;a href="https://lectures.schallbert.de" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Vorträge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; verfügbar.&lt;/p&gt;&#10;&lt;p&gt;Die Rückmeldungen waren überwältigend positiv. Der Vortrag gewährt tiefe Einblicke in die aktuelle Lage der Automobilindustrie und hilft dabei zu verstehen, warum sich die &amp;ldquo;klassischen&amp;rdquo; Autobauer so schwer tun, mit den modernen Fahrzeugen aus Fernost mitzuhalten.&lt;/p&gt;&#10;</description></item><item><title>Jekyll-dockerimage: Bundler und Gemfile</title><link>https://blog.schallbert.de/bundler-ci-gemfile-issue/</link><pubDate>Fri, 14 Nov 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/bundler-ci-gemfile-issue/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-11-15-bundler-errors.avif"&#10; class="post-cover"&#10; alt="Image: bundler logo with error overlay"&#10; title="Jekyll-dockerimage: Bundler und Gemfile" /&gt;&#10;&lt;h2 id="was-ist-bundler"&gt;Was ist &lt;em&gt;bundler&lt;/em&gt;?&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://bundler.io/guides/faq.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;Bundler&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist ein Werkzeug, mit dem Abhängigkeiten zwischen Modulen und Bibliotheken in der Programmiersprache &lt;a href="https://www.ruby-lang.org/en/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;Ruby&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; verwaltet und versioniert werden können. Ich benutze &lt;em&gt;Bundler&lt;/em&gt;-Kommandos regelmäßig, z.B. um meinen Blog zu bauen und auf dem Server live zu stellen.&lt;/p&gt;&#10;&lt;p&gt;Will ich lokal bauen, so tippe ich beispielsweise&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;bundle exec jekyll serve --incremental --future&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;in die Konsole. Dieser Befehl sagt &lt;em&gt;Bundler&lt;/em&gt;, es soll die Applikation &lt;em&gt;jekyll&lt;/em&gt; im Server-Modus ausführen und ihr Parameter mitgeben, welche &lt;em&gt;jekyll&lt;/em&gt; nicht bei jeder Dateiänderung komplett neu bauen lässt und noch nicht veröffentlichte Seiten dennoch mit erstellt.&lt;/p&gt;&#10;&lt;h2 id="bundler-in-der-ci-pipeline"&gt;&lt;em&gt;bundler&lt;/em&gt; in der CI-Pipeline&lt;/h2&gt;&#10;&lt;p&gt;Auch für die Veröffentlichung meiner Seite nutze ich &lt;em&gt;Bundler&lt;/em&gt; als Teil eines Jekyll-Dockerimages, wie bereits öfters auf meiner Seite verlinkt, z.B. bei der Umstellung auf &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/"&gt;self-hosted&lt;/a&gt;. Seit ich auf eine neue Version der Blog-Software umgestiegen bin, sehe ich in meiner CI-Pipeline rätselhafte Fehler. Lokal baut das System jedoch einwandfrei. Wie man solche Fehler angeht und behebt, habe ich hier festgehalten.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;There was an error while trying to write to /path/to/Gemfile.lock&amp;rdquo; - CI console output&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="keine-schreibrechte"&gt;Keine Schreibrechte&lt;/h3&gt;&#10;&lt;p&gt;&lt;em&gt;Bundler&lt;/em&gt; selbst kann auf meiner CI wie der &lt;em&gt;Jekyll&lt;/em&gt;-user auch nur Dateien lesen und nicht schreiben. Was ich für den &lt;em&gt;Jekyll&lt;/em&gt; output mit &lt;code&gt;chown&lt;/code&gt; beheben kann, möchte ich &lt;em&gt;bundler&lt;/em&gt; aber gar nicht erlauben: Ich will ja gerade, dass &lt;code&gt;Gemfile.lock&lt;/code&gt; zwischen meiner lokalen Bauumgebung und der CI identisch bleibt, damit ich mögliche Fehler schon vorab beheben kann.&lt;/p&gt;&#10;&lt;h3 id="spezifikationsdatei-für-bundler"&gt;Spezifikationsdatei für &lt;em&gt;bundler&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;In der Datei &lt;code&gt;Gemfile.lock&lt;/code&gt; stehen alle Abhängigkeiten der verwendeten Applikation drin, inkl. Versionsnummer und Bezugsquelle. Wird nur der Quellcode der Anwendung plus diese Datei geliefert, so kann &lt;em&gt;bundler&lt;/em&gt; eine konkrete Version aller Abhängigkeiten beim Bauen ziehen, sodass unabhängig von Maschine und Version mit ähnlichen Voraussetzungen gearbeitet werden kann.&lt;/p&gt;&#10;&lt;h3 id="fehlerbehebung"&gt;Fehlerbehebung&lt;/h3&gt;&#10;&lt;p&gt;Ein erster Versuch der Behebung durch Angleichung sämtlicher Versionen zwischen lokal und CI schlug fehl.&#10;Der zweite Versuch, dem Jekyll-user Schreibrechte auf die &lt;code&gt;Gemfile.lock&lt;/code&gt; zu geben, schlug ebenfalls fehl.&#10;Der dritte Versuch führte mich zur Bundler-Website, wo ich die Parameter genauer unter die Lupe nahm, Stichwort &amp;ldquo;frozen&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;Es ist nämlich so, dass man &lt;em&gt;bundler&lt;/em&gt; verbieten kann, die &lt;code&gt;Gemfile.lock&lt;/code&gt; umzuschreiben. Dafür verwendet man das Kommando&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;bundle config set frozen true&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Laut &lt;a href="https://bundler.io/v2.7/man/bundle-config.1.html#LIST-OF-AVAILABLE-KEYS" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dokumentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; wird die Verarbeitung abgebrochen, sobald die Datei umgeschrieben werden müsste.&lt;/p&gt;&#10;&lt;p&gt;Obwohl ich diesen Befehl meiner &lt;code&gt;yaml&lt;/code&gt;-Datei in der CI hinzugefügt hatte, schlug das Bauen erneut fehl. Auslöser war dieselbe Bibliothek &lt;a href="https://nokogiri.org/#" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;nokogiri&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; wie vorher, nur war die Fehlermeldung nun viel hilfreicher:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-11-14-gemfile-lock-incompatible.avif" alt="Image: jekyll build error due to build platform incompatibility between local and remote."&gt;&lt;/figure&gt;&#10;&lt;h2 id="gemfilelock-für-verschiedene-umgebungen-optimieren"&gt;&lt;code&gt;Gemfile.lock&lt;/code&gt; für verschiedene Umgebungen optimieren&lt;/h2&gt;&#10;&lt;p&gt;Also befolge ich den Vorschlag und führe das gewünschte Kommando auf meinem lokalen Rechner aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;bundle lock --add-platform x86_64_musl &lt;span style="color:#75715e"&gt;# my CI runner&amp;#39;s environment&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Randnotiz: &lt;a href="https://musl.libc.org/about.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;musl&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist eine &lt;em&gt;libc&lt;/em&gt;-Implementierung für Linux, sprich mit ihrer Hilfe laufen viele in &lt;code&gt;C&lt;/code&gt; geschriebene Programme.&lt;/p&gt;&#10;&lt;p&gt;In meiner Lockfile taucht daraufhin folgender Eintrag auf:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;nokogiri (1.18.10-x86_64-linux-musl)&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Nun läuft meine CI wieder sauber durch und nebenbei habe ich die Portabilität meines Jekll-Setups verbessert.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-11-14-jekyll-yml-freeze-bundler.avif" alt="Image: successful CI run with updated dependencies"&gt;&lt;/figure&gt;&#10;</description></item><item><title>fail2ban: Update zerstört Konfiguration</title><link>https://blog.schallbert.de/fail2ban-error-configuration-bad-value/</link><pubDate>Thu, 23 Oct 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/fail2ban-error-configuration-bad-value/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-23-docker-chain-thumb.avif"&#10; class="post-cover"&#10; alt="Image: iptables view for docker-user chain"&#10; title="fail2ban: Update zerstört Konfiguration" /&gt;&#10;&lt;p&gt;Alle paar Wochen sehe ich auf meinem Server üblicherweise nach dem Rechten:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Ist bei Docker alles ok? &lt;code&gt;docker ps&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Werden die Backups korrekt erzeugt? &lt;code&gt;borgmatic repo-info&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Funktionieren meine Abwehrmaßnahmen? &lt;code&gt;iptables -n -L&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Laufen meine Daemons zur Update-Automation? &lt;code&gt;htop -F /server-config&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Dieses mal allerdings zeigte &lt;code&gt;iptables&lt;/code&gt; mir gähnende Leere. Das ist ungewöhnlich und deutet darauf hin, dass mit &lt;em&gt;fail2ban&lt;/em&gt; etwas nicht stimmt.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# iptables -n -L&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Chain FORWARD &lt;span style="color:#f92672"&gt;(&lt;/span&gt;policy DROP&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;target prot opt source destination &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DOCKER-USER all -- 0.0.0.0/0 0.0.0.0/0 &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DOCKER-FORWARD all -- 0.0.0.0/0 0.0.0.0/0 &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Chain DOCKER-USER &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; references&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;target port opt source destination &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Übersetzt lautet die Ausgabe etwa in etwa&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Die &lt;code&gt;FORWARD&lt;/code&gt;-Chain leitet wie von &lt;em&gt;Docker&lt;/em&gt; benötigt an die für Container relevante &lt;code&gt;DOCKER-USER&lt;/code&gt;-Chain weiter. Unter dieser sind allerdings keine Anwendungen gelistet, die Pakete zurückweisen oder deren Weiterleitung verbieten (&lt;code&gt;REJECT/DROP&lt;/code&gt;) können.&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;An dieser Stelle müssten zwei Einträge für &lt;code&gt;caddy-server&lt;/code&gt; und &lt;code&gt;caddy-ratelimit&lt;/code&gt; auftauchen.&lt;/p&gt;&#10;&lt;h2 id="fail2ban-error-failed-during-configuration"&gt;&amp;ldquo;fail2ban ERROR Failed during configuration&amp;rdquo;&lt;/h2&gt;&#10;&lt;p&gt;Also schaue ich zuerst mal bei Docker nach:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker logs -t fail2ban &lt;span style="color:#75715e"&gt;# Logs für den Container mit Namen fail2ban mit Zeitempel ausgeben&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Statt wie üblich nach dem Durchscrollen mit &lt;code&gt;&amp;lt;timestamp&amp;gt; Server ready&lt;/code&gt; belohnt zu werden, bekomme eine nicht enden wollende Flut an Logmeldungen angezeigt. Sie wiederholen sich und zeigen, dass &lt;em&gt;fail2ban&lt;/em&gt; mit &lt;code&gt;Exit with code 255&lt;/code&gt; aussteigt. Einen besseren Überblick liefert mir das Log, wenn ich nur die letzten 100 Zeilen ausgebe:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker logs -t --tail &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt; fail2ban &lt;span style="color:#75715e"&gt;# Nur die letzten 100 Zeilen des Logs ausgeben, mit Zeitstempel&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="die-fehlermeldung"&gt;Die Fehlermeldung&lt;/h3&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;&amp;lt;timestamp, ID&amp;gt; ERROR Failed during configuration: Bad value substitution: &#10;option &amp;#39;action&amp;#39; in section &amp;#39;caddy-ratelimit&amp;#39; contains an interpolation key &amp;#39;banaction&amp;#39; which is not a valid option name. &#10;Raw value: &amp;#39;%(action_)s&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Die Meldung ist interessant, weil ich genau an dieser Stelle auch schon beim Erstellen im Zuge des Artikels &lt;a href="https://blog.schallbert.de/fail2ban-with-caddy/#check-jail"&gt;Fail2ban mit Caddy&lt;/a&gt; Probleme mit der &lt;code&gt;jail.local&lt;/code&gt; hatte. Damals musste ich &lt;code&gt;action = iptables-multiport&lt;/code&gt; aus der Konfiguration nehmen, weil die auslösenden IP-Adressen auf der falschen &lt;code&gt;CHAIN&lt;/code&gt; (INPUT) gelandet waren.&lt;/p&gt;&#10;&lt;p&gt;Nun scheint also die default-action auf der &lt;code&gt;DOCKER-USER&lt;/code&gt; chain nicht mehr zu funktionieren. Also gehe ich ins Netz und suche nach ein paar Quellen für eine mögliche Lösung. Ich finde aber weder in der Update-Historie von &lt;em&gt;fail2ban&lt;/em&gt; noch in der von mir benutzten Distribution von &lt;a href="https://docs.linuxserver.io/images/docker-fail2ban/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;linuxserver.io&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Hinweise auf Updates, die hier große Änderungen im &lt;code&gt;action.d&lt;/code&gt; Ordner veranlassen würden.&lt;/p&gt;&#10;&lt;h3 id="lösung-per-banaction--iptablestypemultiport"&gt;Lösung per &lt;code&gt;banaction = iptables[type=multiport]&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Also bemühe ich die Websuche und finde einen Kommentar eines &lt;a href="https://github.com/fail2ban/fail2ban/issues/3894" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Maintainers von fail2ban&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;[&amp;hellip;] With action = iptables[type=multiport, protocol=tcp, chain=DOCKER-USER] you overwrote the default action [&amp;hellip;] [Solution:] simply set the chain variable only in the jail [&amp;hellip;]: &lt;code&gt;port = http, https banaction = iptables[type=multiport] chain = DOCKER-USER&lt;/code&gt;&amp;rdquo; - &lt;a href="https://github.com/sebres" target="_blank" rel="noopener noreferrer" class="external-link"&gt;sebres&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, Dec-2024&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Die Grundlage dieser Aussage kann man in den &lt;a href="https://docs.docker.com/engine/network/packet-filtering-firewalls/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker Docs&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; nachlesen. Nebenbemerkung: An allen Stellen wird empfohlen, für zukünftige Anwendungen das modernere &lt;em&gt;nftables&lt;/em&gt; statt &lt;em&gt;iptables&lt;/em&gt; zu verwenden. &lt;a href="https://linux-audit.com/networking/nftables/differences-between-iptables-and-nftables-explained/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hier&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gibt es einen Artikel zum Thema.&lt;/p&gt;&#10;&lt;p&gt;Somit kann ich das bisher in meiner &lt;code&gt;jail.local&lt;/code&gt; vorhandene &lt;code&gt;action&lt;/code&gt;-Feld komplett löschen und stattdessen &lt;code&gt;banaction&lt;/code&gt; verwenden. Sie sieht nun wie folgt aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# jail.local update Oct-2025&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Server/Client errors: 401 403 404 500&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;caddy-status]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;enabled = true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;chain = DOCKER-USER&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;port = http,https&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;filter = caddy-status&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;logpath = &amp;lt;path/to/logfiles&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;findtime = 10min&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;maxretry = 5&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;bantime = 1d&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;banaction = iptables[type=multiport]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mit dieser Konfiguration fährt &lt;em&gt;fail2ban&lt;/em&gt; normal hoch und gibt &lt;code&gt;Server ready&lt;/code&gt; aus.&lt;/p&gt;&#10;&lt;h3 id="überprüfen-der-lösung-mittels-iptables"&gt;Überprüfen der Lösung mittels &lt;em&gt;iptables&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Ich erzeuge mir wie oben beschrieben Einträge in den Test-Logfiles, welche die Aktionen von &lt;code&gt;caddy-status&lt;/code&gt; und &lt;code&gt;caddy-ratelimit&lt;/code&gt; auslösen sollen. Wie erwartet erhalte ich in der &amp;ldquo;blocklist&amp;rdquo;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# iptables -n -L&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Chain DOCKER-USER &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; references&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;target prot opt source destination &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;f2b-caddy-ratelimit tcp -- 0.0.0.0/0 0.0.0.0/0 multiport dports 80,443&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;f2b-caddy-status tcp -- 0.0.0.0/0 0.0.0.0/0 multiport dports 80,443&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>passtdas auf Github</title><link>https://blog.schallbert.de/announcements/2025-09-14-passtdas/</link><pubDate>Sun, 14 Sep 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/announcements/2025-09-14-passtdas/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-passtdas-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Visualizazion of how passtdas analyzes and determines workpiece edges"&#10; title="passtdas auf Github" /&gt;&#10;&lt;h2 id="-september-2025--passtdas-zum-download-verfügbar"&gt;&amp;mdash; September 2025 &amp;mdash; passtdas zum Download verfügbar&lt;/h2&gt;&#10;&lt;p&gt;Das Werkzeug &lt;a href="https://blog.schallbert.de/passtdas/"&gt;passtdas&lt;/a&gt; liest eine G-Code Datei für die Bearbeitung mit einer CNC-Fräsmaschine und ermittelt die Werkstückgrenzen (Extremwerte) in X und Y.&#10;Anschließend schreibt es ein weiteres CNC-Programm mit diesen Punkten und ermöglicht so, dass die Werkstückgrenzen direkt an der Maschine angefahren werden können.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-ext.avif" alt="Image: Visual example of an extreme value dataset for a simple geometry"&gt;&lt;/figure&gt;&#10;&lt;p&gt;So kann stets sichergestellt werden, dass der Rohling korrekt ausgerichtet ist und das Werkstück drauf passt.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/Schallbert/passtdas" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Link zum Projekt auf Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Ich habe vor, das Projekt demnächst in eine Browser-Lösung zu portieren und will es auf diese Weise einfacher zugänglich machen.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Schallbert&lt;/em&gt;&lt;/p&gt;&#10;</description></item><item><title>passtdas - ein Hilfsprogramm für die CNC</title><link>https://blog.schallbert.de/passtdas/</link><pubDate>Mon, 08 Sep 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/passtdas/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-passtdas-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Visualizazion of how passtdas analyzes and determines workpiece edges"&#10; title="passtdas - ein Hilfsprogramm für die CNC" /&gt;&#10;&lt;h2 id="das-problem"&gt;Das Problem&lt;/h2&gt;&#10;&lt;p&gt;Ein Bekannter von mir schrieb mich an. Er betreibt eine CNC-Portalfräsmaschine als Hobby so wie ich und fertigt oft Einzelstücke. Die dabei entstehenden Reste sind aber teils zu schade zum Wegwerfen. Für eine bestmögliche Materialausnutzung probiert er, diese Reste als Rohlinge für weitere Projekte zu verwenden.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Weißt du, wie ich an der Maschine herausfinden kann, ob mein Frästeil auf den Rohling passt?&amp;rdquo; - Anonym&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Schnell ist es passiert: Den Maßen zufolge passt das Werkstück so gerade auf den Rohling. Setzt man den Nullpunkt nur leicht falsch, so fährt der Fräser über die Werkstückgrenzen hinaus und man kann das Werkstück wegwerfen.&lt;/p&gt;&#10;&lt;h2 id="die-lösung"&gt;Die Lösung&lt;/h2&gt;&#10;&lt;p&gt;Es wäre doch toll, wenn die Maschine vor Beginn des Fräsvorganges die Kontur des Werkstückes abfahren würde, oder? So könnte man unkompliziert herausfinden, ob sich der Rohling wirklich komplett innerhalb der Werkstückgrenze befindet.&lt;/p&gt;&#10;&lt;p&gt;Da ich auf Anhieb keine schnelle Lösung online finden kann, denke ich an selbstgeschriebene Software für die Umsetzung. Daher mache ich mich an die Arbeit.&lt;/p&gt;&#10;&lt;p&gt;TODO: ADD VIDEO LINK&lt;/p&gt;&#10;&lt;h2 id="die-software"&gt;Die Software&lt;/h2&gt;&#10;&lt;p&gt;Für einen schnellen Prototypen wähle ich die Hochsprache &lt;a href="https://www.python.org/downloads/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;python&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Warum? Ich beherrsche &lt;em&gt;python&lt;/em&gt; recht gut, Bibliotheken für die erforderlichen geometrischen Berechnungen sind verfügbar, und das geschriebene Programm lässt sich leicht in eine all-in-one-Datei kompilieren.&lt;/p&gt;&#10;&lt;p&gt;Das Programm soll Maschineninstruktionen (G-Code) lesen und die Fräspfade analysieren. Die Stellen, an denen im Fräsprogramm die maximalen und minimalen &lt;code&gt;X- und Y-Werte&lt;/code&gt; angefahren werden, sollen abgespeichert und selbst als Bewegungskommando in einer Pfaddatei ausgegeben werden.&lt;/p&gt;&#10;&lt;p&gt;Lädt man dieses Programm auf die Maschine, so wird ein so ermittelter &amp;ldquo;Extremwert&amp;rdquo; angefahren und die Z-Achse von Sicherheits- auf Messhöhe abgesenkt. Anschließend wird das Programm pausiert, sodass der Bediener Anpassungen der Werkstückposition vornehmen kann. Erst durch Einabe eines weiteren Startbefehls wird der nächste Extremwert angefahren usw.&lt;/p&gt;&#10;&lt;p&gt;Dieses Programm wird &lt;em&gt;passtdas&lt;/em&gt; heißen.&lt;/p&gt;&#10;&lt;h3 id="programmart"&gt;Programmart&lt;/h3&gt;&#10;&lt;p&gt;Ich entscheide mich für ein Konsolenprogramm mit wenigen Parametern:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Shortcut&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Parameter name&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Description&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;-f&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;--file&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;The source file to be analyzed. Required.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;-s&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;--zsafety&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Safety height the machine will do flyovers with. Optional.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;-p&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;--zprobe&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Probe height the machine will pinpoint to. Optional.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Die beiden Höhenparameter bekommen Default-Werte, sodass sie nicht immer mit angegeben werden müssen.&lt;/p&gt;&#10;&lt;h3 id="eingabe"&gt;Eingabe&lt;/h3&gt;&#10;&lt;p&gt;&lt;em&gt;passtdas&lt;/em&gt; liest Dateien ein und durchsucht sie zeilenweise nach Bewegungsbefehlen &lt;code&gt;G00, G01, G02, G03&lt;/code&gt;. Während die Auswertung für lineare Bewegungen wie &lt;code&gt;G00, G01&lt;/code&gt; einfach ist - Lineare Bewegungen lassen sich durch Punkt-zu-Punkt-Verbindungen ausdrücken, deren Extremwerte nicht zwischen den Punkten liegen können - kann das für Kreissegmente durchaus herausfordernd sein.&lt;/p&gt;&#10;&lt;p&gt;Die Höheninformation auf der Z-Achse wird ebenfalls mitgelesen und gespeichert. So kann die Zerspanungstechnikerin schnell prüfen, ob die maximale Frästiefe mit der Gewünschten übereinstimmt.&lt;/p&gt;&#10;&lt;p&gt;Befinden sich in der G-Code-Datei keine oder falsch formulierte Anweisungen, so gibt das &lt;em&gt;passtdas&lt;/em&gt; entsprechende Fehlermeldungen aus und bricht die Analyse ab.&lt;/p&gt;&#10;&lt;h3 id="definition-kreissegmente"&gt;Definition Kreissegmente&lt;/h3&gt;&#10;&lt;p&gt;Zum Hintergrund: Im G-Code können Kreissegmente auf zwei Arten definiert werden.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 X5.660 Y-0.000 Z10.000&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G02 X-5.660 Y0.000 R5.660 F1600&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Kommando:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Fahre einen Kreisbogen im Uhrzeigersinn ab dem Punkt &lt;code&gt;X5.66 Y0&lt;/code&gt; mit Radius &lt;code&gt;5.66mm&lt;/code&gt; bis zum Punkt &lt;code&gt;X-5.66 Y0&lt;/code&gt; mit Vorschub &lt;code&gt;1600mm/min&lt;/code&gt; auf Höhe &lt;code&gt;10mm&lt;/code&gt;.&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Zeichnet man diese Kommandos, entsteht ein hängender Halbkreis. Kreise können mittels dreier Punkte definiert werden. Doch nur zwei Punkte sind gegeben. Der dritte Punkt muss mit Hilfe von Anfangs- und Endpunkt aus dem ebenfalls angegebenen Kreisradius berechnet werden.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-arcr.avif" alt="Image: drawing an arc using given radius, start- and endpoint"&gt;&lt;/figure&gt;&#10;&lt;p&gt;&lt;em&gt;Aufgabe: Finde heraus, wo dieser Kreis seine Extremwerte in XY hat.&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;Aber auch so kann ein Kreissegment definiert sein:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 X-2.5696 Y5.0431&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G03 X-4.6258 Y3.2616 Z-0.5000 I2.5696 J-5.0431&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Kommando:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Fahre einen Kreisbogen gegen den Uhrzeigersinn ab dem Punkt &lt;code&gt;X-2.56 Y5.04&lt;/code&gt; mit Mittelpunktkoordinaten &lt;code&gt;I2.56 J-5.04&lt;/code&gt; bis zum Punkt &lt;code&gt;X-4.62 Y3.26&lt;/code&gt;.&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Zeichnet man diese Kommandos, entsteht ein kurzes Kreissegment im oberen linken Quadranten. Hier sind zwar drei Punkte (Start- und Endpunkt sowie Kreismittelpunkt) gegeben. Doch auch hier kann es mitunter schwierig werden, herauszufinden, ob irgendwo auf dem Kreisbogen ein Extremwert angefahren wird.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-arcij.avif" alt="Image: drawing an arc using center point, start- and endpoint"&gt;&lt;/figure&gt;&#10;&lt;p&gt;&lt;em&gt;Aufgabe: Finde heraus, wo dieser Kreis seine Extremwerte in XY hat.&lt;/em&gt;&lt;/p&gt;&#10;&lt;h3 id="extremwerte-von-kreissegmenten-berechnen"&gt;Extremwerte von Kreissegmenten berechnen&lt;/h3&gt;&#10;&lt;p&gt;Ein Kreissegment kann je nach Spanne bis zu vier zusätzliche Extremwerte haben: &lt;code&gt;X+, Y+, X-, Y-&lt;/code&gt;. Bei einem Vollkreis sind dies stets die Koordinatenschnittpunkte vom Radius aus gesehen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;get_extremes_from_arc&lt;/span&gt;(arc, coordinates):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; x_plus_radius &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [center_x &lt;span style="color:#f92672"&gt;+&lt;/span&gt; radius, center_y, xyz[&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;]]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; y_plus_radius &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [center_x, center_y &lt;span style="color:#f92672"&gt;+&lt;/span&gt; radius, xyz[&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;]]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; x_minus_radius &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [center_x &lt;span style="color:#f92672"&gt;-&lt;/span&gt; radius, center_y, xyz[&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;]]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; y_minus_radius &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [center_x, center_y &lt;span style="color:#f92672"&gt;-&lt;/span&gt; radius, xyz[&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;]]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; extremevalue_order &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [x_plus_radius, y_plus_radius, x_minus_radius, y_minus_radius]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (arc[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;-&lt;/span&gt; arc[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;]) &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# crossing the 0° line (x-axis), handle overflow with nested if below&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; arc[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;360&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; []&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; crossing_angle &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;721&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;90&lt;/span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; crossing_angle &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(int(arc[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;]), int(arc[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; i &lt;span style="color:#f92672"&gt;=&lt;/span&gt; int(crossing_angle &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;90&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; i &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; i &lt;span style="color:#f92672"&gt;-=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; result&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(extremevalue_order[i])&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Sind Kreismittelpunkt sowie Kreisradius bekannt, kann aus den Anfangs- und Endpunkten der überstrichene Winkel bestimmt werden. Hierdurch wird nun klar, ob Kreisanfangspunkt und/oder -Endpunkt selbst Extremwerte darstellen, oder ob durch Überstreichen eines Koordinatennullpunktes zusätzliche Extremwerte entstehen. Genau dies tut das obige Codebeispiel.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-arcext.avif" alt="Image: Possible coordinate extreme values of an arc segment"&gt;&lt;/figure&gt;&#10;&lt;h3 id="datentabelle"&gt;Datentabelle&lt;/h3&gt;&#10;&lt;p&gt;Alle möglichen Extremwerte (zwei bei einer Linie, bis zu vier bei einem Kreissegment) werden nun in einer Datentabelle gesammelt. Sobald jede G-Codezeile ausgewertet und die Liste damit komplett ist, werden die Maximal- und Minimalwerte für jede Achse aus dieser Liste bestimmt.&lt;/p&gt;&#10;&lt;p&gt;So entstehen schließlich vier &lt;code&gt;XYZ&lt;/code&gt;-Koordinaten, welche für jede Raumrichtung die maximale Ausdehnung des Werkstückes beschreibt.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-ext.avif" alt="Image: Visual example of an extreme value dataset for a simple geometry"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Parallel durchsucht das Programm alle Befehle an die Z-Achse und merkt sich die maximale Eintauchtiefe.&lt;/p&gt;&#10;&lt;h3 id="ausgabe"&gt;Ausgabe&lt;/h3&gt;&#10;&lt;p&gt;Das Kommandozeilenprogramm verfügt über keine visuelle Ausgabe. Daher beschränke ich mich auf die Erstellung eines Simulationsprogramms für die Fräsmaschine, in dem die Werkstückausdehnung angefahren wird und bei jedem Punkt pausiert:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G90&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Go to coordinate zero&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;MSG &lt;span style="color:#e6db74"&gt;&amp;#34;Zmin of this job: -1.0&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 Z40&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 X0 Y0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G01 Z15 F1200&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;MSG &lt;span style="color:#e6db74"&gt;&amp;#34;PathPreview: Hit START to go to Ymin: [&amp;#39;0.0&amp;#39;, &amp;#39;-5.66&amp;#39;]&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M00&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 Z40&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 X0.0 Y-5.66&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G01 Z15 F1200&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;MSG &lt;span style="color:#e6db74"&gt;&amp;#34;PathPreview: Hit START to go to Xmin: [&amp;#39;-5.66&amp;#39;, &amp;#39;0.0&amp;#39;]&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M00&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 Z40&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 X-5.66 Y0.0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G01 Z15 F1200&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;MSG &lt;span style="color:#e6db74"&gt;&amp;#34;PathPreview: Hit START to go to Ymax: [&amp;#39;0.0&amp;#39;, &amp;#39;5.66&amp;#39;]&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M00&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 Z40&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 X0.0 Y5.66&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G01 Z15 F1200&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;MSG &lt;span style="color:#e6db74"&gt;&amp;#34;PathPreview: Hit START to go to Xmax: [&amp;#39;5.66&amp;#39;, &amp;#39;0.0&amp;#39;]&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M00&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 Z40&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 X5.66 Y0.0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G01 Z15 F1200&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Obiges Beispiel fährt die Extremwerte eines Kreises mit &lt;code&gt;r=5.66mm&lt;/code&gt; ab. Dabei fährt es stets eine Sicherheitshöhe von &lt;code&gt;Z40&lt;/code&gt; zum Überstreichen der Punkte an und bewegt die Z-Achse bei Ankunft langsam mit &lt;code&gt;F1200&lt;/code&gt; auf die Zielhöhe von &lt;code&gt;Z15&lt;/code&gt;. Wichtig dabei ist, dass der Nullpunkt in &lt;code&gt;Z&lt;/code&gt; vorher korrekt vermessen wurde - sonst kommt es möglicherweise zur Kollision mit dem Werkstück.&lt;/p&gt;&#10;&lt;h2 id="test-und-verifikation"&gt;Test und Verifikation&lt;/h2&gt;&#10;&lt;p&gt;Um zu sehen ob das Programm auch wie erwartet funktioniert habe ich drei Testdateien erstellt:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Unit tests: Prüft einzelne Funktionen im Programm, z.B. ob Linien- und Kreissegmente korrekt erkannt werden&lt;/li&gt;&#10;&lt;li&gt;integration tests: Testen, ob Funktionen im Verbund korrekt funktionieren, z.B. die richtige Berechnung von Extremwerten anhand von Eingabewerten&lt;/li&gt;&#10;&lt;li&gt;End to end tests: Diese Tests prüfen über die gesamte Funktionskette, z.B. ob falsch formatierte Eingabedateien erkannt werden&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-unittest.avif" alt="Image: Test result summary for passtdas"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Zum Schluss erstelle ich mehrere Testdateien mit dem Programm und fahre die daraus erzeugten Extremwerte mit meiner CNC an.&lt;/p&gt;&#10;&lt;h2 id="open-source-software"&gt;Open-source software&lt;/h2&gt;&#10;&lt;p&gt;Der Quellcode für &lt;em&gt;passtdas&lt;/em&gt; steht kostenlos auf &lt;a href="https://github.com/Schallbert/passtdas" target="_blank" rel="noopener noreferrer" class="external-link"&gt;GitHub&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Verfügung, muss allerdings durch Python interpretiert werden.&lt;/p&gt;&#10;&lt;p&gt;Ich baue ein Endanwenderfreundliches und leicht zu bedienendes Programm daraus, sobald genügend Leute den Wunsch dafür geäußert haben.&lt;/p&gt;&#10;</description></item><item><title>Giteas *act_runner* rootless ausführen</title><link>https://blog.schallbert.de/gitea-act-runner-dind/</link><pubDate>Sun, 10 Aug 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/gitea-act-runner-dind/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-08-10-gitea-act-runner-dind-rootless-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Symbolized Docker containers next to text &amp;#39;&amp;#39;rootless act_runner DinD&amp;#39;&amp;#39;"&#10; title="Giteas *act_runner* rootless ausführen" /&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Gitea schickt `act_runner` in Rente&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-09-15T00:00:00Z"&gt;&#10; 15. Sept. 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Dieser Artikel bezieht sich auf eine Actions-Implementierung von Gitea, den &lt;code&gt;act_runner&lt;/code&gt;. Jener ist abgeleitet von &lt;a href="https://github.com/nektos/act" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nectos/act&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Gitea verwendet inzwischen &lt;a href="https://blog.gitea.com/release-of-runner-1.0.0/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;einen eigenen Runner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Der alte Runner sollte ausgetauscht werden. Weitere Infos: Blogartikel &lt;a href="https://blog.schallbert.de/build-deploy-hugo-with-actions-docker-caddy/"&gt;Hugo-Seite mit Gitea Actions, docker, und caddy bereitstellen&lt;/a&gt;&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;p&gt;In diesem Artikel zeige ich, wie ich den &lt;em&gt;act_runner&lt;/em&gt; meiner &lt;em&gt;Gitea&lt;/em&gt;-Instanz von &lt;code&gt;gitea/act_runner&lt;/code&gt; auf &lt;code&gt;gitea/act_runner:latest-dind-rootless&lt;/code&gt; umstelle.&lt;/p&gt;&#10;&lt;p&gt;Warum dieser Aufwand? Weil &lt;code&gt;act_runner&lt;/code&gt; in &lt;em&gt;Docker&lt;/em&gt; läuft und Zugriff auf den Daemon per &lt;code&gt;/var/run/docker.sock&lt;/code&gt; als sogenanntes Volume (gemeint im Sinne von Datenträger) bzw. Bind-Mount benötigt, um zu funktionieren. Der Besitzer dieses Sockets ist &lt;code&gt;root&lt;/code&gt;, wodurch der Container praktisch Vollzugriff auf das Hostsystem erhält.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;RULE #1 - Do not expose the Docker daemon socket (even to the containers)&amp;rdquo; - OWASP / Docker Security Cheat Sheet&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Dies ist hoch riskant, denn es liegt im Prinzip des Runners, von Dritten erzeugten Job-Code auszuführen. Ein offenes Einfallstor. In Verbindung mit direktem Durchgriff auf das Hostsystem kann das im Falle eines erfolgreichen Angriffes einen Totalausfall bzw. die &amp;ldquo;feindliche Übernahme&amp;rdquo; meiner Infrastruktur bedeuten.&lt;/p&gt;&#10;&lt;h2 id="gefahr-durch-dockersock"&gt;Gefahr durch &lt;code&gt;docker.sock&lt;/code&gt;&lt;/h2&gt;&#10;&lt;p&gt;Ich richte mich in dieser Anleitung nach der Empfehlung von &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#docker-security-cheat-sheet" target="_blank" rel="noopener noreferrer" class="external-link"&gt;OWASP zum Thema Docker-Sicherheit&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und wende deren Regel(n) in der Praxis an. OWASP steht für &amp;ldquo;Open Web Application Security Project&amp;rdquo;. Es ist eine Organisation, die sich für höhere Sicherheit von Webanwendungen einsetzt und dabei Anwender wie mich mit kostenfreien Artikeln, Dokumentation und Technologie unterstützt.&lt;/p&gt;&#10;&lt;p&gt;Die unter &lt;strong&gt;Regel 1&lt;/strong&gt; beschriebenen Maßnahmen lauten:&lt;/p&gt;&#10;&lt;h3 id="dockers-tcp-socket-abgeschaltet-lassen"&gt;Dockers &lt;em&gt;tcp&lt;/em&gt; Socket abgeschaltet lassen&lt;/h3&gt;&#10;&lt;p&gt;Sollte der Zugang zum &lt;em&gt;Docker Daemon&lt;/em&gt; per &lt;em&gt;tcp&lt;/em&gt; eingeschaltet sein, kann sich mit diesem - sofern keine weiteren Vorkehrungen getroffen wurden - über eine ungesicherte Verbindung und ohne Authentisierung verbunden werden. Der Daemon ist dann durch praktisch jeden Internetnutzer zugänglich und damit angreifbar.&lt;/p&gt;&#10;&lt;p&gt;Wie stelle ich also sicher, dass der &lt;em&gt;tcp&lt;/em&gt; Socket abgeschaltet ist?&#10;Die Anleitung hierzu findet sich in den &lt;a href="https://docs.docker.com/engine/daemon/remote-access/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker Docs&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und muss quasi verkehrt herum angewandt werden. Auch dort wird ausdrücklich vor ungeschützter Verfügbarmachung des &lt;em&gt;tcp&lt;/em&gt;-Sockets gewarnt.&lt;/p&gt;&#10;&lt;p&gt;Nach Ausführen aller erforderlichen Schritte lasse ich &lt;a href="https://en.wikipedia.org/wiki/Netstat" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;netstat&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; einmal durchlaufen. Es listet mir offene Sockets, Netzwerkschnittstellen und Routing-Tabellen auf:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# check if Docker Daemon&amp;#39;s &amp;#34;dockerd&amp;#34; tcp socket is exposed&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# netstat -lntp | grep dockerd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# test is a pass if this command does not return anything.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bei mir findet sich kein Eintrag mit &lt;code&gt;dockerd&lt;/code&gt;. Mein Server ist an dieser Stelle schon einmal nicht verwundbar.&lt;/p&gt;&#10;&lt;h3 id="den-docker-socket-varrundockersock-nicht-in-andere-container-einbinden"&gt;Den Docker Socket &lt;code&gt;/var/run/docker.sock&lt;/code&gt; nicht in andere Container einbinden&lt;/h3&gt;&#10;&lt;p&gt;Hier wird es schon etwas schwieriger: &lt;em&gt;act_runner&lt;/em&gt; benötigt den Socket, um Job-Container zu erzeugen, zu verwalten und schließlich wieder zu entsorgen. Ohne Zugriff auf den &lt;em&gt;Docker Daemon&lt;/em&gt; über den Socket funktioniert die Build-Pipeline schlicht und einfach nicht - es sei denn, man möchte komplett auf &lt;em&gt;Docker&lt;/em&gt; verzichten und sowohl den Runner als auch die Build-Jobs direkt auf der Host-Maschine laufen lassen. Damit gingen aber viele Nachteile einher: Verlust der Kapselung, keine Portabilität, schlechte Skalierbarkeit, geringere Sicherheit&amp;hellip;&lt;/p&gt;&#10;&lt;p&gt;Aber auch für die Option mit &lt;em&gt;Docker&lt;/em&gt; gibt es eine Lösung: &lt;a href="https://gitea.com/gitea/act_runner/src/branch/main/examples/docker-compose#running-act_runner-using-docker-in-docker-dind" target="_blank" rel="noopener noreferrer" class="external-link"&gt;act_runner Docker-in-Docker aufsetzen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Mit diesem Setup erhält &lt;em&gt;act_runner&lt;/em&gt; einen eigenen &lt;em&gt;Docker Daemon&lt;/em&gt;, jedoch mit eingeschränkten Rechten und ohne Zugriff auf den des Host-Systems. Dieser übernimmt dann den Lebenszyklus von Job-Containern, sodass sie komplett unabhängig vom Host-System laufen.&lt;/p&gt;&#10;&lt;h2 id="docker-in-docker"&gt;Docker in Docker&lt;/h2&gt;&#10;&lt;p&gt;Folgendes Schaubild verdeutlicht den Unterschied:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-08-10-runner-dind-setup.avif" alt="Image: act_runner with standard configuration versus DinD-rootless. The latter has an isolated Docker Daemon running within the act_runner container."&gt;&lt;/figure&gt;&#10;&lt;h3 id="act_runner-rootless-starten"&gt;Act_runner &amp;ldquo;rootless&amp;rdquo; starten&lt;/h3&gt;&#10;&lt;p&gt;Also folge ich der Anleitung und kopiere mir eine passende &lt;code&gt;docker-compose.yml&lt;/code&gt; zusammen. Wichtig hier:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Es muss &lt;code&gt;privileged: true&lt;/code&gt; gesetzt werden. Ansonsten kann der Docker Daemon im &lt;em&gt;act_runner&lt;/em&gt;-Container nicht richtig starten, da ihm Kernel-Funktionen fehlen. Somit schmiert der ganze Container wieder und wieder ab, ohne dabei hilfreiche Fehlermeldungen zu erzeugen.&lt;/li&gt;&#10;&lt;li&gt;Die Umgebungsvariable &lt;code&gt;DOCKER_HOST=unix:///var/run/user/1000/docker.sock&lt;/code&gt; muss gesetzt sein. Hier wird der Docker Socket über einen nicht-privilegierten User gesteuert und steht dem Runner für die Verwaltung von Job Containern zur Verfügung.&#10;Der Daemon läuft gekapselt im Container und steht nicht in Verbindung mit der Host-Maschine.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-08-10-gitea-act-runner-dind-rootless.avif" alt="Image: DinD-rootless runner is now working fine"&gt;&lt;/figure&gt;&#10;&lt;h3 id="volume-verwirrung"&gt;Volume-Verwirrung&lt;/h3&gt;&#10;&lt;p&gt;Prima! Da der Runner nun läuft, lasse ich ihn direkt mal auf einen Job los. Leider schlägt das Bauen bereits nach Sekundenbruchteilen mit dieser Meldung fehl:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;# Runner step: Set up job&#10;failed to start container: Error response from daemon: error while creating mount source path &amp;#39;&amp;lt;volumeSourceFullPath&amp;gt;&amp;#39;: mkdir &amp;lt;volumeSourcePath&amp;gt;: permission denied&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hier habe ich stundenlang recherchieren müssen und war lange der falschen Überzeugung, dass es an fehlenden Berechtigungen bei Ordnern auf der Host-Maschine liegt. Später erst verstand ich wirklich, dass Docker-in-Docker genau so gemeint ist, wie es heißt: Nicht nur werden Container durch Container erzeugt, sondern innerhalb des Containers läuft ein separater Docker Daemon!&lt;/p&gt;&#10;&lt;p&gt;Damit funktioniert nämlich der klassische Weg nicht mehr, Volumes in Job Containern direkt vom Host-System per &lt;code&gt;-v /a/b:/x/y&lt;/code&gt; verfügbar zu machen.&#10;Stattdessen müssen Volumes nun durchgereicht werden. Beispiel:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;em&gt;host&lt;/em&gt; directory &lt;code&gt;/opt/server/www/blog-artifacts&lt;/code&gt; -&amp;gt; &lt;em&gt;act_runner&lt;/em&gt; volume &lt;code&gt;/tmp/blog-artifacts&lt;/code&gt; &amp;ndash;&amp;gt; &lt;em&gt;job&lt;/em&gt; container volume &lt;code&gt;/tmp/blog-artifacts&lt;/code&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Das &lt;code&gt;:z&lt;/code&gt; im Volume vom &lt;em&gt;act_runner&lt;/em&gt; wird jetzt wichtig. Es zeigt &lt;em&gt;Docker&lt;/em&gt; gegenüber an, dass dieses Volume zwischen Containern geteilt wird. Diese Volumes müssen nicht nur in der &lt;code&gt;docker-compose.yml&lt;/code&gt; von &lt;em&gt;act_runner&lt;/em&gt; angegeben werden, sondern auch die Job-Skripte im Ordner &lt;code&gt;.gitea/workflows/&lt;/code&gt; sind jetzt entsprechend anzupassen. Hier wird das &lt;code&gt;:z&lt;/code&gt; auf der &amp;ldquo;rechten Seite&amp;rdquo; hingegen nicht benötigt.&lt;/p&gt;&#10;&lt;h3 id="is-not-a-valid-volume"&gt;&amp;ldquo;is not a valid volume&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Allem Aufwand zum Trotz laufen meine Jobs noch immer nicht durch. Dieses Mal wegen einer Fehlermeldung, die &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/#action-volumes"&gt;mir bereits bekannt vorkam&lt;/a&gt;:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log*" data-lang="log*"&gt;# Runner step: Set up job&#10;[/tmp/blog-artifacts] is not a valid volume, will be ignored&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Also rein in die &lt;code&gt;config.yml&lt;/code&gt; von &lt;em&gt;act_runner&lt;/em&gt; und die Volume-Namen hinzufügen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /gitea/runner/config.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;valid_volumes&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;/tmp/blog-artifacts&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/tmp/lectures-artifacts&amp;#34;&lt;/span&gt;]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In dieser Datei kann ich &lt;code&gt;privileged: false&lt;/code&gt; stehen lassen, denn der Job Container selbst benötigt anders als &lt;em&gt;act_runner&lt;/em&gt; keine Kernel-Features.&lt;/p&gt;&#10;&lt;h2 id="berechtigungen-korrekt-setzen"&gt;Berechtigungen korrekt setzen&lt;/h2&gt;&#10;&lt;p&gt;Nun erhalte ich erneut &lt;code&gt;Permission Denied&lt;/code&gt; Fehlermeldungen beim Ausführen meiner Jobs, wenn auch nicht direkt im ersten Schritt der Aktionen. Da ich die Volume-Pfade jetzt bis ins Kleinste geprüft habe, kann es eigentlich nur noch an den Ordnerberechtigungen auf der Host-Maschine liegen.&lt;/p&gt;&#10;&lt;p&gt;Damit die vom Job-Container erzeugten Artefakte durch die Volumes auf meinem Host abgelegt werden können, muss ich das zu schreibende Verzeichnis und alle Unterordner &lt;code&gt;-R&lt;/code&gt; an den vorher definierten, nicht-privilegierten User &lt;code&gt;ID=1000&lt;/code&gt; übergeben:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# chown -R 1000:1000 /target/path/to/artifact/&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Endlich funkioniert alles reibungslos und ich habe der (unwahrscheinlichen, aber möglichen) Übernahme meines Host-Systems durch bösartige Job-Container einen Riegel vorgeschoben.&lt;/p&gt;&#10;</description></item><item><title>Rate limiter für Gitea</title><link>https://blog.schallbert.de/gitea-rate-limiter/</link><pubDate>Fri, 25 Jul 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/gitea-rate-limiter/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-07-25-gitea-rate-limiter-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Gitea&amp;#39;&amp;#39;s logo, a cup of tea, protected by many arrows symbolizing GET requests"&#10; title="Rate limiter für Gitea" /&gt;&#10;&lt;p&gt;Hier halte ich eine Schritt-für-Schritt Anleitung fest, die das Problem meiner &lt;a href="https://blog.schallbert.de/gitea-out-of-memory/"&gt;&lt;em&gt;Gitea&lt;/em&gt;-Abstürze&lt;/a&gt; lösen soll. Die Erfahrung aus der Erstanwendung eines Rate Limiters &lt;a href="https://blog.schallbert.de/fail2ban-with-caddy/"&gt;für meinen Blog&lt;/a&gt; lasse ich hier einfließen mit dem Ziel, dass die Anwendung robuster läuft und gegen ungerichtete Denial of Service Attacken geschützt wird.&lt;/p&gt;&#10;&lt;h2 id="schritt-1-log-quelle-für-rate-limiter-festlegen"&gt;Schritt 1: Log-Quelle für rate limiter festlegen&lt;/h2&gt;&#10;&lt;p&gt;&lt;em&gt;Gitea&lt;/em&gt; erzeugt natürlich selbst Logs, theoretisch bis herunter auf Zugriffsebene durch externe Clients. Doch schaffte ich es in der Vergangenheit nicht, &lt;a href="https://blog.schallbert.de/server-protection/#was-noch-nicht-funktioniert-gitea--fail2ban"&gt;&lt;em&gt;Giteas&lt;/em&gt; Access-Logs aus Docker auszuleiten&lt;/a&gt;. Daher geben wir jetzt dem als Reverse Proxy angeflanschten &lt;em&gt;Caddyserver&lt;/em&gt; die Anweisung, Logs stellvertretend zu erstellen. In ihnen tauchen dann Zugriffe auf das Webinterface von &lt;em&gt;Gitea&lt;/em&gt; auf.&lt;/p&gt;&#10;&lt;p&gt;In der &lt;code&gt;Caddyfile&lt;/code&gt; sieht das Log-Modul total unspektakulär aus.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /caddy/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;log {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; output file &lt;span style="color:#e6db74"&gt;/log/&lt;/span&gt;gitea&lt;span style="color:#f92672"&gt;/&lt;/span&gt;access&lt;span style="color:#f92672"&gt;.&lt;/span&gt;log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="internen-traffic-vom-log-ausschließen"&gt;Internen Traffic vom Log ausschließen&lt;/h3&gt;&#10;&lt;p&gt;Nun schaue ich die Logs mal an und sehe Einträge, die ich gar nicht haben will. So erzeugt &lt;em&gt;act_runner&lt;/em&gt; zum Beispiel alle zwei Sekunden einen &lt;code&gt;Fetch Task POST&lt;/code&gt; nach &lt;em&gt;Gitea&lt;/em&gt; und alle zehn Sekunden einen &lt;code&gt;GET&lt;/code&gt; Request mit demselben Ziel als &lt;a href="https://blog.schallbert.de/fix-gitea-runner/"&gt;Health check&lt;/a&gt;. Diese brauchen für mich im Log gar nicht erst aufzutauchen. Mein erster Gedanke hier war, einfach Anfragen von internen IP-Adressen nicht zu loggen. Da &lt;em&gt;Caddy&lt;/em&gt; allerdings als Reverse Proxy fungiert, laufen ausnahmslos alle IP-Adressen &amp;ldquo;intern&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;Also muss ich &lt;em&gt;act_runner&lt;/em&gt; spezifische Logs anders erkennen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /caddy/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git&lt;span style="color:#f92672"&gt;.&lt;/span&gt;schallbert&lt;span style="color:#f92672"&gt;.&lt;/span&gt;de {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; reverse_proxy &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#e6db74"&gt;http&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;//&lt;/span&gt;&lt;span style="color:#e6db74"&gt;gitea&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Enable logging for fail2ban, don&amp;#39;t log for runner&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log_skip &lt;span style="color:#e6db74"&gt;/api/&lt;/span&gt;actions&lt;span style="color:#f92672"&gt;*&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; output file &lt;span style="color:#e6db74"&gt;/log/&lt;/span&gt;gitea&lt;span style="color:#f92672"&gt;/&lt;/span&gt;access&lt;span style="color:#f92672"&gt;.&lt;/span&gt;log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die &lt;a href="https://caddyserver.com/docs/caddyfile/directives/log_skip" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;log_skip&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Direktive weist &lt;em&gt;caddy&lt;/em&gt; nun an, keine Logs für Zugriffe auf &lt;code&gt;/api/actions*&lt;/code&gt; mehr anzulegen.&lt;/p&gt;&#10;&lt;h3 id="reverse-proxy-remote-ips-anzeigen"&gt;Reverse-Proxy: Remote IPs anzeigen&lt;/h3&gt;&#10;&lt;p&gt;Dennoch habe ich noch immer ein Problem: Wenn im Log alle externen Anfragen auf einer internen IP-Adresse hereinkommen, wie soll ich &amp;ldquo;böse&amp;rdquo; Anfragen dann wegblocken? Eine Websuche &lt;a href="https://caddy.community/t/how-to-get-a-true-remote-ip-behind-caddy-reverse-proxy/22348/2" target="_blank" rel="noopener noreferrer" class="external-link"&gt;zeigt, dass dies für Reverse Proxy in Docker&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ein üblicher Stolperstein ist. Leider helfen mir viele Lösungen nicht, da sie andere Serversoftware wie &lt;em&gt;nginx&lt;/em&gt; verwenden oder andere Services hinter ihren Proxies laufen haben als &lt;em&gt;Gitea.&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;Doch tatsächlich ist es ganz einfach: Nur eine Zeile muss an der richtigen Stelle in die &lt;code&gt;Caddyfile&lt;/code&gt; eingefügt werden und schon kommen die remote IP-Adressen unverfälscht rein.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /caddy/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git&lt;span style="color:#f92672"&gt;.&lt;/span&gt;schallbert&lt;span style="color:#f92672"&gt;.&lt;/span&gt;de {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; reverse_proxy &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#e6db74"&gt;http&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;//&lt;/span&gt;&lt;span style="color:#e6db74"&gt;gitea&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; trusted_proxies &lt;span style="color:#ae81ff"&gt;172&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Docker-internal netwock traffic runs with these IPs&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[...] &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die Direktive &lt;a href="https://caddyserver.com/docs/caddyfile/options#trusted-proxies" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;trusted_proxies&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; teilt &lt;em&gt;Caddy&lt;/em&gt; mit, dass dem von &lt;em&gt;Docker&lt;/em&gt; bereitgestelltem Netzwerk vertraut werden kann. Somit wird die eigentliche Quell-IP angezeigt statt der internen Schnittstelladresse des Containers. Genau das brauche ich, um die Adressen später mit &lt;em&gt;fail2ban&lt;/em&gt; analysieren zu können.&lt;/p&gt;&#10;&lt;h2 id="schritt-2-anwendungsfall-bestimmen"&gt;Schritt 2: Anwendungsfall bestimmen&lt;/h2&gt;&#10;&lt;p&gt;Schauen wir uns an, wie viele HTTP &lt;code&gt;200 ok&lt;/code&gt; Anfragen im Grenzfall zwischen Normalnutzung und &amp;ldquo;abuse&amp;rdquo; kommen. Dazu surfe ich auf &lt;em&gt;Gitea&lt;/em&gt; herum und klicke einen Haufen Dinge an, wie ich das als Mensch sonst nie in der Geschwindigkeit tun würde. Danach mache ich eine Auswertung der Logs.&lt;/p&gt;&#10;&lt;p&gt;Damit haben wir erste Richtwerte für den Rate Limiter.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# rate limiter tests&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;findtime = 10s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;maxretry = 10&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;bantime = 6h&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="schritt-3-fail2ban-konfigurieren"&gt;Schritt 3: Fail2ban konfigurieren&lt;/h2&gt;&#10;&lt;p&gt;Anschließend konfigurieren wir Filter und Jail-Datei von &lt;code&gt;fail2ban&lt;/code&gt; so, dass sie die oben definierten Logs analysieren.&lt;/p&gt;&#10;&lt;h3 id="filter"&gt;Filter&lt;/h3&gt;&#10;&lt;p&gt;Ich verwende hier schlicht denselben Filter für meinen Rate-Limiter aus dem &lt;a href="https://blog.schallbert.de/fail2ban-with-caddy/#ordner-filterd"&gt;vorherigen Artikel&lt;/a&gt; wieder. Uns interessieren nur erfolgreiche Anfragen, die wir innerhalb eines Zeitfensters zählen.&lt;/p&gt;&#10;&lt;h3 id="jail"&gt;Jail&lt;/h3&gt;&#10;&lt;p&gt;Ich verwende die Werte aus den rate limiter Tests 1:1 hier in der Jail-Datei weiter. Als Filter verweise ich auf &lt;code&gt;caddy-ratelimit&lt;/code&gt; (Link oben).&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /fail2ban/config/fail2ban/jail.local&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;gitea-ratelimit]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;enabled = true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;chain = DOCKER-USER&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;port = http,https&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;filter = caddy-ratelimit&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;logpath = /var/log/caddy2/gitea/access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;findtime = 10s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;maxretry = 10&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;bantime = 6h&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mit einem Neustart von &lt;em&gt;fail2ban&lt;/em&gt; schalte ich den Schutz scharf.&lt;/p&gt;&#10;&lt;h2 id="schritt-4-rate-limiter-testen"&gt;Schritt 4: Rate limiter testen&lt;/h2&gt;&#10;&lt;p&gt;Hier gehe ich genauso vor wie in meinem Artikel &lt;a href="https://blog.schallbert.de/fail2ban-with-caddy/#test-rate-limiter"&gt;fail2ban with caddy&lt;/a&gt; und komme zum selben Ergebnis. Es funktionert!&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Fehlerbehebung &amp;#39;Bad value substitution&amp;#39;&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2025-10-09T00:00:00Z"&gt;&#10; 09. Okt. 2025&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; In Verbindung mit diesem Artikel gibt es ein wichtiges Update, welches den &lt;a href="https://blog.schallbert.de/fail2ban-error-configuration-bad-value/"&gt;&lt;em&gt;fail2ban&lt;/em&gt; Fehler &amp;lsquo;Bad value substitution&amp;rsquo; for &amp;lsquo;action&amp;rsquo;&lt;/a&gt; behebt. Dieses Problem kann auftauchen, wenn &lt;em&gt;fail2ban&lt;/em&gt; Services prüfen soll, die in Docker-Containern laufen.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;</description></item><item><title>Rate limiter mit Caddy und fail2ban</title><link>https://blog.schallbert.de/fail2ban-with-caddy/</link><pubDate>Thu, 10 Jul 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/fail2ban-with-caddy/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-07-10-fail2ban-ratelimit-thumb.avif"&#10; class="post-cover"&#10; alt="Image: block drawing of how caddy interacts with fail2ban to rate limit accesses to my blog"&#10; title="Rate limiter mit Caddy und fail2ban" /&gt;&#10;&lt;p&gt;Hier beschreibe ich, wie man das meist zur &lt;a href="https://blog.schallbert.de/server-protection/"&gt;Abwehr unerlaubter Zugriffsversuche&lt;/a&gt; eingesetze Werkzeug &lt;em&gt;fail2ban&lt;/em&gt; auch zur Begrenzung der Anzahl von erfolgreichen Zugriffen innerhalb eines definierten Zeitfensters verwenden kann. Dies nennt man &amp;ldquo;rate limiting&amp;rdquo; und soll denial of service Attacken vereiteln, bei denen &lt;a href="https://blog.schallbert.de/gitea-out-of-memory/"&gt;mein Server überlastet&lt;/a&gt; wird.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-07-10-fail2ban-ratelimit.avif" alt="Image: block drawing of how caddy interacts with fail2ban to rate limit accesses to my blog"&gt;&lt;/figure&gt;&#10;&lt;h2 id="caddy-logs-einschalten"&gt;Caddy-Logs einschalten&lt;/h2&gt;&#10;&lt;p&gt;Zuerst müssen wir den Logger des Webservers einschalten. Bei Caddy geht das mit wenigen Zeilen Code, die ich meiner &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/#caddy"&gt;bestehenden Konfiguration&lt;/a&gt; hinzufüge:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# caddy2/config/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;blog.schallbert.de {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Define webserver&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;root * /www/blog&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;encode gzip&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;file_server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Enable logging for fail2ban&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;log {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;output file /log/blog/access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ab jetzt werden im angegebenen Pfad Logs angelegt, die ich mit einer &lt;a href="https://blog.schallbert.de/logrotate-mistake/"&gt;&lt;em&gt;logrotate&lt;/em&gt;-Konfiguration&lt;/a&gt; verwalte.&lt;/p&gt;&#10;&lt;h2 id="fail2ban-als-rate-limiter-konfigurieren"&gt;Fail2ban als Rate Limiter konfigurieren&lt;/h2&gt;&#10;&lt;p&gt;Nochmal kurz zur Wiederholung: &lt;em&gt;fail2ban&lt;/em&gt; greift auf die Paketfilter-Regeln der Maschine zu und modifiziert sozusagen ihre Firewall, um dynamische Angriffe abzuwehren. Damit dies funktioniert, müssen &lt;em&gt;fail2ban&lt;/em&gt; Logdateien zur Verfügung gestellt werden, die eine Host-Applikation wie beispielsweise ein Webserver erzeugt und IP-Adressen der Client-Rechner enthalten.&lt;/p&gt;&#10;&lt;h3 id="vorüberlegung-http-200-ok-als-filter"&gt;Vorüberlegung: &amp;ldquo;http 200 OK&amp;rdquo; als Filter?&lt;/h3&gt;&#10;&lt;p&gt;Auch normale, zulässige Zugriffe &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/200" target="_blank" rel="noopener noreferrer" class="external-link"&gt;200 OK&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; müssen für meinen Rate Limiter zählen.&lt;/p&gt;&#10;&lt;p&gt;Um ein Gefühl zu bekommen wie der Logger Zugriffe aufzeichnet, werfe ich einen Blick in die Logs. Ich lasse mir die Anzahl Einträge zurückgeben, die einen Status &lt;code&gt;200 OK&lt;/code&gt; enthalten. Dazu führe ich mit &lt;em&gt;grep&lt;/em&gt; eine Suche durch und lasse mir die Anzahl Treffer zeilenweise per &lt;code&gt;wc -l&lt;/code&gt; ausrechnen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:/var/log/caddy2/blog# grep -o &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;status&amp;#34;:200&amp;#39;&lt;/span&gt; access.log | wc -l&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;847&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Gut, es hat heute also bereits 847 Zugriffe gegeben, die &lt;code&gt;200 OK&lt;/code&gt; zurückgemeldet bekommen haben. Nun rufe ich selbst mal die Landing page meiner Blogseite auf und lasse mir dann die Artikelübersicht anzeigen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:/var/log/caddy2/blog# grep -o &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;status&amp;#34;:200&amp;#39;&lt;/span&gt; access.log | wc -l&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;858&lt;/span&gt; // after calling blog.schallbert.de&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rschallbert@machine:/var/log/caddy2/blog# grep -o &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;status&amp;#34;:200&amp;#39;&lt;/span&gt; access.log | wc -l&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;882&lt;/span&gt; // page loaded after clicking &lt;span style="color:#e6db74"&gt;&amp;#34;Posts&amp;#34;&lt;/span&gt; button&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Whow, das waren 35 Einträge für zwei Klicks! Wenn ich mir das Log so ansehe, dann erklären sich die vielen Zeilen mit Assets - sprich Bildern und Logos, die geladen werden.&lt;/p&gt;&#10;&lt;p&gt;Damit ist leider klar, dass das Filterkriterium &lt;code&gt;status:200&lt;/code&gt; für meinen Rate Limiter nicht ohne Weiteres funktionieren kann. Denn die Anzahl Meldungen hängen maßgeblich von dem jeweiligen Artikel ab. Ich müsste also eine Grenze definieren, über der kein normaler Mensch Zugriffe auf mein Blog erzeugt.&lt;/p&gt;&#10;&lt;p&gt;Um nur &amp;ldquo;echte&amp;rdquo; Zugriffe auf meine Seiten zu zählen, muss ich die Assets in den Logs irgendwie ausschließen. Glücklicherweise gibt es dafür eine einfache &lt;a href="https://caddyserver.com/docs/caddyfile/directives/log_skip" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Direktive in Caddy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# caddy2/config/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Enable logging for fail2ban&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log_skip &lt;span style="color:#e6db74"&gt;/assets*&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; log {&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; output file /&lt;/span&gt;log&lt;span style="color:#f92672"&gt;/&lt;/span&gt;blog&lt;span style="color:#f92672"&gt;/&lt;/span&gt;access&lt;span style="color:#f92672"&gt;.&lt;/span&gt;log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;log_skip&lt;/code&gt; sorgt nun dafür, dass alle Zugriffe auf Dateien im &lt;code&gt;assets/&lt;/code&gt;-Ordner und darunter nicht mitgeloggt werden.&lt;br&gt;&#10;Das Verhalten von Fail2ban wird mittels zweier Konfigurationsdateien festgelegt:&lt;/p&gt;&#10;&lt;h3 id="ordner-filterd"&gt;Ordner &lt;code&gt;filter.d&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;filter.d/&amp;lt;filter-name&amp;gt;.conf&lt;/code&gt; beinhaltet die Definition eines zu betrachtenden Ereignisses. So kann die &lt;code&gt;failregex&lt;/code&gt; ein &amp;ldquo;Seite nicht gefunden&amp;rdquo; &lt;code&gt;http 404&lt;/code&gt; Statuscode ebenso zum Auslösen des Paketfilters verwendet werden wie der erfolgreiche Zugriff für den Aufbau meines Rate Limiter, welcher auf &lt;code&gt;200 OK&lt;/code&gt; reagiert. Abgeschrieben habe ich für die Regex bei &lt;a href="https://www.kassner.com.br/en/2023/09/10/fail2ban-caddy-json-logs/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Rafael Kassner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /opt/fail2ban/config/filter.d/caddy-ratelimit.conf&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;Definition]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;failregex = &amp;#34;client_ip&amp;#34;:&amp;#34;&amp;lt;HOST&amp;gt;&amp;#34;(.*)&amp;#34;status&amp;#34;:200&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;datepattern = \d+&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;ignoreregex =&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Außerdem werden in der Datei weitere Randbedingungen bestimmt, z.B. das Datumsformat &lt;code&gt;datepattern&lt;/code&gt; an den Log-Output des zu schützenden Systems angepasst.&lt;/p&gt;&#10;&lt;h3 id="datei-jaillocal"&gt;Datei &lt;code&gt;jail.local&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;jail.local&lt;/code&gt; bestimmt die Bedingungen, bei deren Eintritt der Paketfilter für die Client-IP aktiv wird und weitere Zugriffsversuche blockt. Für den Einsatz als Rate Limiter benötige ich folgende Felder:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;findtime&lt;/code&gt; - das Zeitfenster, in welchem Angriffe gezählt werden, &lt;code&gt;maxretry&lt;/code&gt; ist die Anzahl zulässiger Versuche im Zeitfenster und &lt;code&gt;bantime&lt;/code&gt; ist die Zeit, für die geblockt wird. &lt;code&gt;ignoreip&lt;/code&gt; ist meist per Werkseinstellung auf die relevanten internen IP-Adressen vorkonfiguriert. In meinem Falle funkt &lt;em&gt;act_runner&lt;/em&gt; zum Beispiel an intern &lt;em&gt;gitea&lt;/em&gt; zur Abfrage, ob neue Automatisierungsaufgaben anliegen. Diese Zugriffe will ich auf keinen Fall behindern.&lt;/p&gt;&#10;&lt;p&gt;Das im Folgenden verwendete Beispiel für &lt;em&gt;fail2ban&lt;/em&gt; zeigt den Rate Limiter auf meinem Blog. Am Ende sind Dateien und Log-Einträge für &lt;em&gt;gitea&lt;/em&gt; im Wesentlichen gleich. Nur Filternamen und Parameter unterscheiden sich voneinander durch die unterschiedlichen Anforderungen an Webseite und DevOps-Platform.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /opt/fail2ban/jail.local&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;caddy-ratelimit]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;enabled = true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;chain = DOCKER-USER&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;action = iptables-multiport&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;port = http,https&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;filter = caddy-ratelimit&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;logpath = /var/log/caddy2/blog/access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;findtime = 10s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;maxretry = 10&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;bantime = 6h&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Sind diese beiden Dateien konfiguriert, kann man &lt;em&gt;fail2ban&lt;/em&gt; neu starten und sich die Logs anschauen. Wenn man noch Fehler in den Dateien hat, gibt &lt;em&gt;fail2ban&lt;/em&gt; folgendes aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ERROR Errors in jail &lt;span style="color:#e6db74"&gt;&amp;#39;caddy-ratelimit&amp;#39;&lt;/span&gt;. Skipping...&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tritt dies auf, kann z.B. das Jail nicht dem Filter (muss gleichnamig sein!) nicht zugeordnet werden, eine Feld-Definition wie &lt;code&gt;maxretry&lt;/code&gt; ist falsch getippt oder ein Zahlenformat kann nicht gelesen werden. Wenn alles gut läuft, lautet der Eintrag:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Reading config files: /etc/fail2ban/filter.d/caddy-ratelimit.conf&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="test-rate-limiter"&gt;Rate Limiter testen&lt;/h2&gt;&#10;&lt;p&gt;Nun möchte ich prüfen, ob alles wie erwartet funktioniert.&lt;/p&gt;&#10;&lt;h3 id="filter-regex-überprüfen"&gt;Filter-Regex überprüfen&lt;/h3&gt;&#10;&lt;p&gt;Als ersten Schritt schaue ich auf den Filterbegriff und prüfe, ob er in den Logs zuverlässig gefunden wird. Praktischerweise hat &lt;em&gt;fail2ban&lt;/em&gt; hierfür ein passendes Werkzeug eingebaut: &lt;code&gt;fail2ban-regex &amp;lt;logfile&amp;gt; &amp;lt;filter&amp;gt;&lt;/code&gt;. Ich kann also schlicht die Filterdatei und ein Test-Logfile eingeben und sehe dann, ob ich Treffer bekomme.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;fail2ban-regex ./access.log /etc/fail2ban/filter.d/caddy-ratelimit.conf &#10;&#10;Running tests&#10;=============&#10;&#10;Use filter file : caddy-ratelimit, basedir: /etc/fail2ban&#10;Use datepattern : \d+ : \d+&#10;Use log file : ./access.log.2&#10;Use encoding : UTF-8&#10;&#10;Results&#10;=======&#10;&#10;Failregex: 2009 total&#10;|- #) [# of hits] regular expression&#10;| 1) [2009] &amp;#34;client_ip&amp;#34;:&amp;#34;&amp;lt;HOST&amp;gt;&amp;#34;(.*)&amp;#34;status&amp;#34;:200&#10;`-&#10;&#10;Ignoreregex: 0 total&#10;&#10;Date template hits:&#10;|- [# of hits] date format&#10;| [2685] \d+&#10;`-&#10;&#10;Lines: 2685 lines, 0 ignored, 2009 matched, 676 missed&#10;[processed in 0.12 sec]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Quantitativ bekomme ich, was ich erwarte: das oben beschriebene Suchverfahren per &lt;em&gt;grep&lt;/em&gt; liefert mir exakt dieselben Ergebnisse.&lt;/p&gt;&#10;&lt;h3 id="check-jail"&gt;Jail überprüfen&lt;/h3&gt;&#10;&lt;p&gt;Als nächsten Schritt muss ich testen, ob der Rate Limiter auch greift. Dafür simuliere ich mir Logeinträge. Die Jail-Regeln nehme ich für diesen Test stark zurück, sonst muss ich zu viele Eingaben in kurzer Zeit erzeugen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /opt/fail2ban/jail.local TEST&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;findtime = 10s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;maxretry = 2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;bantime = 100s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nach einem Neustart von &lt;em&gt;fail2ban&lt;/em&gt; erzeuge ich mir eine zweite Konsole, wo ich Log-Einträge simulieren kann. Dabei müssen mindestens Zeitpunkt, IP-Adresse des Aufrufenden und Status enthalten sein; ich gehe aber auf Nummer sicher und nehme vollständige Einträge.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#39;{&amp;#34;ts&amp;#34;:1751246139,&amp;#34;remote_ip&amp;#34;:&amp;#34;1.1.1.1&amp;#34;,&amp;#34;status&amp;#34;:200,[superLongIrrelevantOtherStuffForFiltering]}&amp;#39;&lt;/span&gt; &amp;gt;&amp;gt; /var/log/caddy/blog/access.log&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dies gebe ich mehrfach ein, um über mein Limit von 2 retries zu kommen.&lt;/p&gt;&#10;&lt;p&gt;In der ersten Konsoleninstanz prüfe ich anschließend den &lt;em&gt;fail2ban&lt;/em&gt;-Status für den entsprechenden Filter.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fail2ban-client status caddy-ratelimit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Status &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; the jail: caddy-ratelimit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;|- Filter&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| |- Currently failed:&#9;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| |- Total failed:&#9;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- File list:&#9;/var/log/caddy2/blog/access.log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- Actions&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |- Currently banned:&#9;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |- Total banned:&#9;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- Banned IP list:&#9;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das war nichts. Warum offenbart die fail2ban-Logdatei:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /fail2ban/config/log/fail2ban/fail2ban.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WARN &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Ignoring all log entries older than 20s; &lt;span style="color:#75715e"&gt;# probably messages generated within a fail2ban restart period&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Also nochmal mit passenden Zeitstempeln. Diese muss ich aus der aktuellen Systemzeit (&lt;code&gt;$(date +%s.%N)&lt;/code&gt;) ziehen, wenn ich sie ständig anpassen muss. Daher nehme ich eine vollständige Logzeile her, modifiziere sie auf die IP-Adresse &lt;code&gt;1.1.1.1&lt;/code&gt; und füge passende Zeitstempel ein:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;echo&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;{\&amp;#34;level\&amp;#34;:\&amp;#34;info\&amp;#34;,\&amp;#34;ts\&amp;#34;:$(date +%s.%N),\&amp;#34;logger\&amp;#34;:\&amp;#34;http.log.access.log0\&amp;#34;,\&amp;#34;msg\&amp;#34;:\&amp;#34;handled request\&amp;#34;,\&amp;#34;request\&amp;#34;:{\&amp;#34;remote_ip\&amp;#34;:\&amp;#34;1.1.1.1\&amp;#34;,\&amp;#34;remote_port\&amp;#34;:\&amp;#34;38229\&amp;#34;,\&amp;#34;client_ip\&amp;#34;:\&amp;#34;1.1.1.1\&amp;#34;,\&amp;#34;proto\&amp;#34;:\&amp;#34;HTTP/1.1\&amp;#34;,\&amp;#34;method\&amp;#34;:\&amp;#34;GET\&amp;#34;,\&amp;#34;host\&amp;#34;:\&amp;#34;blog.schallbert.de\&amp;#34;,\&amp;#34;uri\&amp;#34;:\&amp;#34;/vacuum-clamping/\&amp;#34;,\&amp;#34;headers\&amp;#34;:{\&amp;#34;Accept-Encoding\&amp;#34;:[\&amp;#34;gzip, deflate, br\&amp;#34;],\&amp;#34;Connection\&amp;#34;:[\&amp;#34;keep-alive\&amp;#34;],\&amp;#34;User-Agent\&amp;#34;:[\&amp;#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0\&amp;#34;],\&amp;#34;Accept\&amp;#34;:[\&amp;#34;text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\&amp;#34;],\&amp;#34;Accept-Language\&amp;#34;:[\&amp;#34;en-US,en;q=0.5\&amp;#34;]},\&amp;#34;tls\&amp;#34;:{\&amp;#34;resumed\&amp;#34;:false,\&amp;#34;version\&amp;#34;:772,\&amp;#34;cipher_suite\&amp;#34;:4865,\&amp;#34;proto\&amp;#34;:\&amp;#34;http/1.1\&amp;#34;,\&amp;#34;server_name\&amp;#34;:\&amp;#34;blog.schallbert.de\&amp;#34;}},\&amp;#34;bytes_read\&amp;#34;:0,\&amp;#34;user_id\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;duration\&amp;#34;:0.001207354,\&amp;#34;size\&amp;#34;:12290,\&amp;#34;status\&amp;#34;:200,\&amp;#34;resp_headers\&amp;#34;:{\&amp;#34;Last-Modified\&amp;#34;:[\&amp;#34;Tue, 10 Jun 2025 19:33:01 GMT\&amp;#34;],\&amp;#34;Content-Encoding\&amp;#34;:[\&amp;#34;gzip\&amp;#34;],\&amp;#34;Server\&amp;#34;:[\&amp;#34;Caddy\&amp;#34;],\&amp;#34;Alt-Svc\&amp;#34;:[\&amp;#34;h3=\\\&amp;#34;:443\\\&amp;#34;; ma=2592000\&amp;#34;],\&amp;#34;Vary\&amp;#34;:[\&amp;#34;Accept-Encoding\&amp;#34;],\&amp;#34;Etag\&amp;#34;:[\&amp;#34;\\\&amp;#34;gzip\\\&amp;#34;\&amp;#34;],\&amp;#34;Content-Type\&amp;#34;:[\&amp;#34;text/html; charset=utf-8\&amp;#34;]}}&amp;#34;&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nach mehrmaligem Aufruf dieses gebastelten Kommandos erhalte ich nun&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fail2ban-client status caddy-ratelimit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Status &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; the jail: caddy-ratelimit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;|- Filter&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| |- Currently failed:&#9;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| |- Total failed:&#9;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- File list:&#9;/var/log/caddy2/blog/access.log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- Actions&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |- Currently banned:&#9;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |- Total banned:&#9;&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- Banned IP list:&#9;1.1.1.1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das sieht super aus. Auf zum letzten Schritt.&lt;/p&gt;&#10;&lt;h3 id="paketfilter-überprüfen"&gt;Paketfilter überprüfen&lt;/h3&gt;&#10;&lt;p&gt;Wenn wir nun einmal in die Logs schauen, müsste unser &lt;code&gt;[caddy-ratelimit]&lt;/code&gt; Jail nun bei jeder &amp;ldquo;normalen&amp;rdquo; Anfrage triggern.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine: nano fail2ban/config/log/fail2ban/fail2ban.log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP a&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP b&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Ignore 172.18.0.1 by ip&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Ignore 172.18.0.1 by ip&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP c&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP c&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP c&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP c&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP d&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP d&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP e&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP e&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP f&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Um die ganze Kette getestet zu haben müssen wir jetzt noch prüfen, ob die entsprechende IP-Adresse auch wirklich in der Hardware geblockt ist. Dafür gebe ich ein:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# iptables -n -L | grep &lt;span style="color:#e6db74"&gt;&amp;#34;1.1.1.1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;REJECT all -- 1.1.1.1 0.0.0.0/0 reject-with icmp-port-unreachable&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Scheint ja alles in Ordnung zu sein. Für einen letzten Test von einer externen IP-Adresse rufe ich im Browser einen &lt;a href="https://www.deadlinkchecker.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Link Checker&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; auf, der meine Webseite crawlen und damit den Rate Limiter auslösen soll. Obwohl dieser 44 Links in kürzester Zeit durchläuft, wird der Test als &amp;ldquo;bestanden&amp;rdquo; angezeigt. Sehr merkwürdig, &lt;code&gt;[caddy-ratelimit]&lt;/code&gt; hätte anschlagen müssen!&lt;/p&gt;&#10;&lt;h3 id="iptable-chain-input-statt-forward"&gt;iptable-chain INPUT statt FORWARD&lt;/h3&gt;&#10;&lt;p&gt;Verwirrt schaue ich mir mal die ganze &lt;code&gt;iptable&lt;/code&gt; an. Parallel suche ich mir die IP-Adresse des Link Checker heraus. Sie taucht in den &lt;code&gt;iptables&lt;/code&gt; auf. Und trotzdem wird offensichtlich nicht geblockt. Woran das liegt? Ein genauer Blick zeigt:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;Chain INPUT (policy ACCEPT)&#10;target prot opt source destination &#10;f2b-caddy-ratelimit tcp -- 0.0.0.0/0 0.0.0.0/0 multiport dports 22&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Problem hier ist, dass mein Jail auf der &lt;code&gt;INPUT&lt;/code&gt;-Chain liegt. Die Anfragen gehen aber doch nicht direkt an meine Server-Hardware, sondern werden über &lt;em&gt;Docker&lt;/em&gt; an &lt;em&gt;Caddyserver&lt;/em&gt; weitergeleitet. Um zu funktionieren, muss ich auf der &lt;code&gt;FORWARD&lt;/code&gt;-Chain landen, wo &lt;code&gt;DOCKER-USER&lt;/code&gt; bereits ist. Sehr merkwürdig, hatte ich doch in der &lt;code&gt;jail.local&lt;/code&gt; extra &lt;code&gt;chain = DOCKER-USER&lt;/code&gt; angegeben. Irgend etwas muss diese Definition überschreiben.&lt;/p&gt;&#10;&lt;p&gt;Ein unscheinbarer &lt;a href="https://gist.github.com/Rankarusu/23a04ed587b05c6f2b701f2457a127b0?permalink_comment_id=5347631#gistcomment-5347631" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Forenpost&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zusammen mit dem Kommentar &lt;code&gt;multiport&lt;/code&gt; im iptables-Print bringt mich auf die Lösung: Die &lt;code&gt;action = iptables-multiport&lt;/code&gt; Direktive überschreibt mein &lt;code&gt;chain = DOCKER-USER&lt;/code&gt; Statement, denn in der zugehörigen Konfigurationsdatei &lt;code&gt;iptables.conf&lt;/code&gt; wird folgendes gesetzt: &lt;code&gt;chain = INPUT&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Also lösche ich den &lt;code&gt;action&lt;/code&gt; Eintrag einfach, sodass &lt;em&gt;fail2ban&lt;/em&gt; auf den Default für &lt;code&gt;DOCKER-USER&lt;/code&gt; zurückfällt: &lt;code&gt;multiport dports 80,443&lt;/code&gt;.&#10;Nun taucht der Jail-Name auch korrekt in &lt;code&gt;iptables&lt;/code&gt; auf.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# iptables -n -L&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Chain DOCKER-USER &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; references&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;target prot opt source destination &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;f2b-caddy-ratelimit tcp -- 0.0.0.0/0 0.0.0.0/0 multiport dports 80,443&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="erfolg-genießen"&gt;Erfolg genießen&lt;/h2&gt;&#10;&lt;p&gt;Endlich zeigt ein weiterer Link Check, dass mein Rate Limiter funktioniert.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-07-10-deadlinkcheck-ratelimit-working.avif" alt="Image: deadlinkchecker view for blog.schallbert.de hits configured rate limit and gets blocked subsequentially. Thus it returns a Timeout"&gt;&lt;/figure&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Fehlerbehebung &amp;#39;Bad value substitution&amp;#39;&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2025-10-09T00:00:00Z"&gt;&#10; 09. Okt. 2025&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; In Verbindung mit diesem Artikel gibt es ein wichtiges Update, welches den &lt;a href="https://blog.schallbert.de/fail2ban-error-configuration-bad-value/"&gt;&lt;em&gt;fail2ban&lt;/em&gt; Fehler &amp;lsquo;Bad value substitution&amp;rsquo; for &amp;lsquo;action&amp;rsquo;&lt;/a&gt; behebt. Dieses Problem kann auftauchen, wenn &lt;em&gt;fail2ban&lt;/em&gt; Services prüfen soll, die in Docker-Containern laufen.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;</description></item><item><title>Gitea stürzt ab: Zu viele Anfragen</title><link>https://blog.schallbert.de/gitea-out-of-memory/</link><pubDate>Mon, 30 Jun 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/gitea-out-of-memory/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-server-load-thumb.avif"&#10; class="post-cover"&#10; alt="Image: server at max load when gitea is flooded with GET requests"&#10; title="Gitea stürzt ab: Zu viele Anfragen" /&gt;&#10;&lt;p&gt;Hier möchte ich kurz darstellen, wie ein Absturz eines Dienstes meinen Server über Stunden wiederholt lahmgelegt hat. Und dies so gründlich, dass ich nur aus der Konsole des Anbieters überhaupt einen Neustart durchführen konnte. Ich erkläre, wie es dazu kam und wie ich dieses und ähnliche Probleme in Zukunft vermeiden möchte.&lt;/p&gt;&#10;&lt;h2 id="ich-wurde-angegriffen-oder"&gt;Ich wurde angegriffen. Oder?&lt;/h2&gt;&#10;&lt;p&gt;Ich saß gerade an einem Artikel, den ich später posten wollte. Zur Sicherheit erstellte ich wie üblich einen Commit und wollte ihn auf meine &lt;em&gt;Gitea&lt;/em&gt;-Instanz schieben. Doch mein &lt;code&gt;git push&lt;/code&gt; Befehl lief einfach nicht durch.&lt;/p&gt;&#10;&lt;p&gt;Verwundert versuchte ich, meine Webseite aufzurufen. Zurück kam:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-404-message.avif" alt="Image: A browser&amp;#39;s timeout error message `Could not complete your request`"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Merkwürdig. Anschließend wollte ich mich auf meinem Server einloggen, um nach dem Rechten zu sehen: &lt;code&gt;ssh &amp;lt;servername&amp;gt;&lt;/code&gt;. Wieder blieb das Terminal ohne Reaktion. Mist!&lt;/p&gt;&#10;&lt;p&gt;Timeout. Als letzte Rettung meldete ich mich bei meinem Hoster an und schaute auf die Graphen des Servers:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-server-load.avif" alt="Image: 200% CPU load on my server for nearly two hours"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Oh, was ist denn da los? Nun versuche ich über die Weboberfläche, den Server herunterzufahren. Klappt auch nicht. Erst ein harter Neustart ist erfolgreich.&#10;Ich kann mich wieder per SSH einloggen und sehe, dass auch alle &lt;em&gt;Docker&lt;/em&gt;-Container wieder ganz normal hochfahren.&lt;/p&gt;&#10;&lt;h2 id="was-war-passiert"&gt;Was war passiert?&lt;/h2&gt;&#10;&lt;p&gt;Gut, dass ich eine Woche lang alle Logs aufbewahre. So und über die Auslastung der Maschine über Zeit kann ich einigermaßen rekonstruieren, was passiert ist.&lt;/p&gt;&#10;&lt;h3 id="welche-logs-helfen"&gt;Welche Logs helfen?&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Crash lokalisieren! Auslösende Anwendung finden im &lt;code&gt;kern.log&lt;/code&gt;, Zeitstempel notieren.&lt;/li&gt;&#10;&lt;li&gt;Gibt es systemweite Auswirkungen oder andere Dienste, die beeinträchtigt werden? Im &lt;code&gt;syslog&lt;/code&gt; nachsehen.&lt;/li&gt;&#10;&lt;li&gt;Falls der Verdacht aufkommt, das System sei möglicherweise gehackt worden: &lt;code&gt;auth.log&lt;/code&gt; hat die Details.&lt;/li&gt;&#10;&lt;li&gt;Sollte die betroffene Anwendung im Container laufen, helfen entsprechende Logs dort möglicherweise weiter.&lt;/li&gt;&#10;&lt;li&gt;Logs der Anwendung selbst sichten. Besonders die Zeit kurz vor dem Crash ist interessant.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Und hier nochmal im Detail die Logs, die ich mir für meine Verhalten angesehen habe und wo sie zu finden sind.&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;log name&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;purpose&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;relevant content for this issue&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;/var/log/syslog&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;System-wide (bare metal) messages&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(docker) warning: &amp;ldquo;health check for container &lt;code&gt;&amp;lt;ID&amp;gt;&lt;/code&gt; timeout&amp;rdquo; (containerd) error: &amp;ldquo;ttrpc: received message on inactive stream&amp;rdquo;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;/var/log/auth.log&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Contains authentication messages from external hosts, in my case mostly SSH&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;None&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;/var/log/kern.log&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Logs all app, service, daemon and system crashes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Out of memory: Killed process &lt;code&gt;&amp;lt;ID&amp;gt;&lt;/code&gt; (&lt;code&gt;&amp;lt;serviceName&amp;gt;&lt;/code&gt;)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[.]/gitea/log/gitea.log&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Protocol for requests, actions on Gitea&amp;rsquo;s web interface, repo changes etc.&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;GET&lt;/code&gt; requests, crash/restart indications&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;docker container logs &lt;code&gt;&amp;lt;containerID&amp;gt;&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Docker&amp;rsquo;s logs for the container in question&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Received signal 15; terminating. (SIGTERM)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="kernlogsyslog"&gt;kern.log/syslog&lt;/h3&gt;&#10;&lt;p&gt;Hier sehe ich neben dem Stack trace, was genau passiert ist. Die am besten verständlichste Meldung ist folgende:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;Out of memory: Killed process `&amp;lt;ID&amp;gt;` (gitea)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Gitea ist also der Stecker gezogen worden, weil es praktisch alle Systemressourcen gezogen hatte.&#10;Wenn ich im Log nach oben scrolle, sehe ich ein paar Minuten vorher:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;timestamp&amp;gt; &amp;lt;machineName&amp;gt; dockerd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;716&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;warning msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Health check for container &amp;lt;ID&amp;gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;timestamp&amp;gt; &amp;lt;machineName&amp;gt; dockerd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;716&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;error msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stream copy error: reading from a closed fifo&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bereits hier sehe ich Warnungen, dass der gitea-Container nicht sauber arbeitet. Ein weiterer Vorteil, den das Einbauen von &amp;ldquo;Healthchecks&amp;rdquo; in der &lt;code&gt;docker-compose.yml&lt;/code&gt; Datei hat. Über Zweck und Einbau von Healthchecks in &lt;em&gt;docker&lt;/em&gt; hatte ich aus anderen Gründen bereits &lt;a href="https://blog.schallbert.de/fix-gitea-runner/#healthcheck"&gt;einen Artikel verfasst&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="gitealog"&gt;gitea.log&lt;/h3&gt;&#10;&lt;p&gt;Folgender Log-Eintrag zeigt wegen eines &lt;a href="https://github.com/go-gitea/gitea/issues/22275" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Bugs in einem Submodul&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; indirekt an, dass &lt;em&gt;Gitea&lt;/em&gt; gerade neu gestartet ist:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cmd/web.go:205:serveInstalled&lt;span style="color:#f92672"&gt;()&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;W&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Table system_setting Column version db default is , struct default is &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und weiter oben im Log nun:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-attack-log.avif" alt="Image: Gitea log showing numerous GET request entries, many of which for a specific large file (MiB range), following a typical Gitea startup message"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Sehr interessant. Die &lt;code&gt;highlight.css&lt;/code&gt; liegt in meinem öffentlichen Repo. Auf ihrer Grundlage erhält die Seite &lt;a href="https://lectures.schallbert.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;lectures.schallbert.de&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ihr Aussehen. Diese Datei ist ziemlich groß, fast &lt;code&gt;1MiB&lt;/code&gt;. Und sie wird hier gleich zig male geladen, und das praktisch für jeden Commit.&lt;/p&gt;&#10;&lt;p&gt;Nun schaue ich mir weitere Crashes in der Vergangenheit an. Immer sind es bursts von &lt;code&gt;GET&lt;/code&gt;-Befehlen auf große Dateien oder die Anforderung von &lt;code&gt;compares&lt;/code&gt; zwischen zwei Branches des Repository, die einem Aufhängen meines Servers vorausgehen.&lt;/p&gt;&#10;&lt;h2 id="wer-steckt-dahinter"&gt;Wer steckt dahinter?&lt;/h2&gt;&#10;&lt;p&gt;Alle crash-auslösenden Requests kommen aus dem selben IP-Adressraum. Die Abstürze starteten bereits vor ein paar Wochen. Zumeist aber zu Tageszeiten, die ich (und anscheinend auch viele meiner LeserInnen) nicht mitbekamen. Und nach ein paar Minuten war der Server stets wieder im Normalbetrieb.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-gcp-dos.avif" alt="Image: Whois-request for the IP that had my server crashed, owner: Google LLC (GCP)"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Oha, die Spur führt zu Google&amp;rsquo;s Cloud Platform (GCP).&lt;/p&gt;&#10;&lt;h3 id="aber-ich-hatte-doch-robots-ausgesperrt"&gt;Aber ich hatte doch Robots ausgesperrt?&lt;/h3&gt;&#10;&lt;p&gt;In der Tat hatte ich die &lt;a href="https://blog.schallbert.de/gitea-search-indexation/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Suchmaschinenindexierung für Gitea gestoppt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Merkwürdig. Oder habe ich es hier gar nicht mit einer der Google-spiders oder &amp;ldquo;KI&amp;rdquo;-Scraper, sondern mit einem fiesen Hacker zu tun, der sich bei &amp;ldquo;Google Cloud&amp;rdquo; eine virtuelle Maschine gemietet hat?&lt;/p&gt;&#10;&lt;h3 id="log-recherche-wie-häufig-wird-die-datei-angefragt"&gt;Log-Recherche: Wie häufig wird die Datei angefragt?&lt;/h3&gt;&#10;&lt;p&gt;Ein Spider würde ja höchstens einmal alle paar Wochen alle meine Seiten durchsuchen, richtig? Und sich hoffentlich nicht über meine &lt;code&gt;robots.txt&lt;/code&gt; hinwegsetzen. Erst recht nicht würde ein Crawler mehrfach und in kurzen Abständen denselben Request stellen, oder?&lt;/p&gt;&#10;&lt;p&gt;Zum dies zu prüfen, suche ich in den *Gitea Logs nach Einträgen von GET-Anfragen auf einer der großen und damit ressourcenintensiv zu übertragenen Dateien:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gunzip gitea.log.&amp;lt;date.rotateID&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;grep &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;filename&amp;gt;&amp;#34;&lt;/span&gt; gitea.log.&amp;lt;date.rotateID&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In der Liste sehe ich, dass dieselbe Anfrage auf dieselbe Zieldatei von derselben IP-Adresse mehrfach und binnen Sekunden gestellt wird. Kurz vor dem Crash benötigt mein Server schon fast &lt;code&gt;4sec&lt;/code&gt;, um die Anfrage zu bedienen.&lt;/p&gt;&#10;&lt;p&gt;Auch sehe ich, dass sich die IP-Adresse alle paar Stunden ändert.&lt;/p&gt;&#10;&lt;h2 id="einen-dos-angriff-parieren"&gt;Einen DoS-Angriff parieren&lt;/h2&gt;&#10;&lt;p&gt;Zusammenfassend muss ich zum Schluss kommen, dass ich aus dem Adressraum des GCP per &lt;a href="https://en.wikipedia.org/wiki/Denial-of-service_attack" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Denial-of-service&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; angegriffen werde.&lt;/p&gt;&#10;&lt;p&gt;Um ein bisschen mehr Hintergrundwissen zu sammeln, besuche ich ein paar Websites zum Thema. Dort lerne ich, dass &lt;em&gt;gitea&lt;/em&gt; auf meinem Server unter einer &lt;a href="https://www.geeksforgeeks.org/computer-networks/types-of-dos-attacks/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Application Layer Attack&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zusammenbricht. Jetzt da ich weiß, was passiert und wie das Problem heißt, fällt das Finden von Lösungen gleich viel leichter 😅&lt;/p&gt;&#10;&lt;p&gt;Ich will mich natürlich nicht kampflos ergeben, indem ich meine Gitea-Instanz dauerhaft vom Netz nehme. Also, welche Optionen habe ich?&lt;/p&gt;&#10;&lt;h3 id="mehr-ressourcen-bereitstellen"&gt;Mehr Ressourcen bereitstellen&lt;/h3&gt;&#10;&lt;p&gt;Zugegeben, meine Maschine hat nur &lt;code&gt;40GB&lt;/code&gt; Speicher und &lt;code&gt;2GB VRAM&lt;/code&gt; sowie eine mickrige 2-Kern CPU aus 2009. Ich könnte einen leistungsfähigeren Server buchen, um Lastspitzen besser abreiten zu können. Doch dies würde den Angriff nicht verhindern, sondern nur seine Auswirkungen abmildern.&lt;/p&gt;&#10;&lt;h3 id="rate-limiting-direkt-im-webserver"&gt;Rate limiting direkt im Webserver&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Rate_limiting" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Rate limiter&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; begrenzen die Anzahl Anfragen eines Clients innerhalb eines Zeitfensters. Dadurch werden die Ressourcen des Servers geschont. Dies ist eine &amp;ldquo;weiche&amp;rdquo; Abwehr von Dos-Attacken, denn auslösende IP-Adressen werden mit einer Fehlermeldung kurzzeitig und sanft abgewehrt. Üblicherweise wird &lt;code&gt;HTTP status code 429&lt;/code&gt; &amp;ldquo;Too Many Requests&amp;rdquo; zurückgegeben, wenn der Limiter eingreift.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Gitea&lt;/em&gt; verfügt über keinen Rate Limiter. In &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/#web-access"&gt;meinem Setup&lt;/a&gt; läuft &lt;em&gt;Gitea&lt;/em&gt; hinter einem reverse proxy, der von meinem &lt;em&gt;Caddyserver&lt;/em&gt; bereitgestellt wird. Hier also müsste ich ansetzen. Für &lt;em&gt;Caddy&lt;/em&gt; gibt es Rate limiter nur als &lt;a href="https://caddyserver.com/docs/modules/http.handlers.rate_limit" target="_blank" rel="noopener noreferrer" class="external-link"&gt;externe Module&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, welche manuell in &lt;em&gt;xcaddy&lt;/em&gt; nachinstalliert und eingerichtet werden müssen.&lt;/p&gt;&#10;&lt;h3 id="blocken-mit-ip-table-regeln"&gt;Blocken mit IP-table Regeln&lt;/h3&gt;&#10;&lt;p&gt;Hier könnte man sich erneut &lt;em&gt;fail2ban&lt;/em&gt; hernehmen und mehrfache Anfragen derselben Ressource von einer IP-Adresse schlicht blocken. &lt;em&gt;Gitea&lt;/em&gt; hat in der &lt;a href="https://docs.gitea.com/next/administration/fail2ban-setup" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dokumentation eine Beschreibung der Einrichtung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. In meinem Falle müsste ich für die Umsetzung dort weitermachen, &lt;a href="https://blog.schallbert.de/server-protection/#was-noch-nicht-funktioniert-gitea--fail2ban"&gt;wo ich bereits einmal aufgegeben hatte&lt;/a&gt; und damit nicht nur SSH, sondern auch normale Seitenaufrufe überwachen.&lt;/p&gt;&#10;&lt;p&gt;Für mich klingt diese Lösung am Sinnvollsten, denn sie trennt klar Funktionen in verschiedene Anwendungen auf. Ich nutze ausschließlich bei mir bereits verfügbare Applikationen: &lt;em&gt;Caddy&lt;/em&gt; würde die Zugriffslogs bereitstellen und &lt;em&gt;fail2ban&lt;/em&gt; müsste sie lesen und Filter in der Konfoguration &lt;code&gt;jail.local&lt;/code&gt; so setzen, dass es sich wie ein Rate Limiter verhält.&lt;/p&gt;&#10;&lt;p&gt;Jetzt schaue ich mal, wie ich &lt;em&gt;Caddy&lt;/em&gt; entsprechend mit &lt;em&gt;fail2ban&lt;/em&gt; verbinden kann. Den weiterführenden Artikel &lt;a href="https://blog.schallbert.de/fail2ban-with-caddy/"&gt;Fail2ban mit Caddy einrichten&lt;/a&gt; verlinke ich direkt mal 🙂&lt;/p&gt;&#10;</description></item><item><title>Webshop selber aufsetzen - Teil3</title><link>https://blog.schallbert.de/ecommerce-selection/</link><pubDate>Sun, 15 Jun 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/ecommerce-selection/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-15-opencart-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Snip of OpenCart&amp;#39;s article about data safety"&#10; title="Webshop selber aufsetzen - Teil3" /&gt;&#10;&lt;h2 id="die-auswahl-anhand-von-leitfragen"&gt;Die Auswahl anhand von Leitfragen&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Welche Lösung ist einfach gestrickt und mit wenig Zeitaufwand zu installieren?&lt;/li&gt;&#10;&lt;li&gt;Welche Lösung ist leichter zu pflegen?&lt;/li&gt;&#10;&lt;li&gt;Bei welchem System kann ich als Betreiber am einfachsten nachvollziehen, wie was funktioniert (Transparenz, Abhängigkeiten)?&lt;/li&gt;&#10;&lt;li&gt;Wo ist die Angriffsoberfläche geringer?&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="hier-fehlt-noch-einiges"&gt;Hier fehlt noch einiges!&lt;/h2&gt;&#10;&lt;p&gt;In den letzten Wochen ist mir die Lust nach einem eigenen Webshop etwas vergangen, da ich mich erst um die Sicherheit meines Servers und die Einarbeitung alter, fast fertiger Projekte kümmern wollte. Ich werde diese Serie vervollständigen, sobald mich das ich-will-was-Neues-machen-Fieber erneut packt.&lt;/p&gt;&#10;&lt;p&gt;Haltet durch!&#10;&lt;em&gt;Schallbert&lt;/em&gt;&lt;/p&gt;&#10;</description></item><item><title>Webshop selber aufsetzen - Teil2</title><link>https://blog.schallbert.de/ecommerce-alternatives/</link><pubDate>Thu, 15 May 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/ecommerce-alternatives/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-05-15-ecommerce-frameworks-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: logos from different e-commerce solutions"&#10; title="Webshop selber aufsetzen - Teil2" /&gt;&#10;&lt;h2 id="ein-paar-optionen-für-die-webshop-software"&gt;Ein paar Optionen für die Webshop-Software&lt;/h2&gt;&#10;&lt;p&gt;Gehen wir mal davon aus, dass in der Vorauswahl nur Software landet, die meine &lt;a href="https://blog.schallbert.de/ecommerce-requirements/"&gt;Anforderungen&lt;/a&gt; zumindest teilweise erfüllt. Im Folgenden möchte ich ein paar mögliche Ecommerce-Lösungen vorstellen.&lt;/p&gt;&#10;&lt;h3 id="shopify"&gt;Shopify&lt;/h3&gt;&#10;&lt;p&gt;Shopify ist eine alles-inklusive-Paketlösung für den Onlinehandel. Shopify Inc. hat ihren Hauptsitz in Kanada und wickelt das europäische Geschäft über Irland ab. Die auf der Website zu findenden Bereiche &lt;a href="https://www.shopify.com/de/legal" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Rechtliches&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; sowie die Komplexität der Informationsdarstellung zum Thema &lt;a href="https://www.shopify.com/de/legal/impressum?country=de&amp;amp;lang=en" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Datenschutz&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; lassen mich vermuten, dass Shopify ein multinationaler Konzern ist - und nicht nur die Onlineshops selbigen zur Verfügung stellt.&lt;/p&gt;&#10;&lt;p&gt;Aber zurück zum Thema.&lt;/p&gt;&#10;&lt;p&gt;Die Domain sowie das Hosting sind bei Shopify im Preis inbegriffen und der eigene Shop kann ohne Programmierkenntnisse per Weboberfläche, Auswählen eines Themes und Einstellen von ein paar Parametern in kurzer Zeit erstellt werden. Shopify besitzt eine ausführliche Dokumentation und bietet Fortbildungen und allerlei weitere Dienstleistungen für ihre Abonnenten an.&lt;/p&gt;&#10;&lt;p&gt;Doch wo bleibt da der Spaß? Schließlich betreibe ich ein Techblog und bin kein Geschäftsmann im eigentlichen Sinne. Ein bisschen Programmieren muss da schon drin sein. Außerdem gefällt mir als Open-Source-Fan das Konzerngehabe (man schaue sich ihre moderne, mit automatisch abspielenden Videos ausgestattete, auf Marketing-Zwecke durchoptimierte Website an) nicht so besonders gut. Die Konfigurierbarkeit ist bestenfalls mittelmäßig und die Optimierung auf Suchmaschinenergebnisse muss ich wie bei den meisten anderen Lösungen auch selbst erledigen. Was Datenschutz und Sicherheit angeht, betreiben sie die übliche Compliance- und Stand-der-Technik-Kommunikation; ich kann also nicht dahinter schauen und muss davon ausgehen, dass sie ihre Kunden auch im eigenen Interesse gut schützen.&lt;/p&gt;&#10;&lt;p&gt;Shopify bezeichnet sich selbst als &lt;a href="https://www.shopify.com/blog/open-source-ecommerce#5" target="_blank" rel="noopener noreferrer" class="external-link"&gt;closed-source SaaS platform&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Daher streiche ich diese Plattform von meiner Liste präferierter Webshoplösungen, weil ich mich nicht ohne Not von Wohl und Wehe Dritter abhängig machen möchte. Bei Shopify hätte nämlich nicht ich die Kontrolle über Abokosten oder &amp;ldquo;meinen&amp;rdquo; Anteil an der Cloud, sondern Shopify. Nicht ich kann bestimmen, wie viele Transaktionen im &amp;ldquo;Plan&amp;rdquo; inklusive sind, sondern Shopify. Nicht meine Kunden blieben Herren ihrer Daten, sondern Shopify oder ein von ihnen mit der Abwicklung beauftragtes Drittunternehmen.&lt;/p&gt;&#10;&lt;h3 id="wordpress--woocommerce"&gt;Wordpress / WooCommerce&lt;/h3&gt;&#10;&lt;p&gt;Ich kenne Wordpress nur als Moloch. Die Eierlegende Wollmilchsau, die durch eine unübersichtlich gewordene Anzahl an Plugins, Erweiterungen und Funktionen alles kann, was man sich nur wünschen kann. Und vieles mehr, was man nicht braucht.&lt;/p&gt;&#10;&lt;p&gt;Aber es ist quelloffen, kostenfrei und wird laut eigener Darstellung von sehr, sehr vielen Shopbetreibern genutzt. Schauen wir uns also mal die bekannteste Ecommerce-Erweiterung zu Wordpress an: &lt;a href="https://woocommerce.com/document/build-online-store/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;WooCommerce&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Ich fand auf Anhieb zig Anleitungen zum Aufsetzen eines WooCommerce-Shops (Beispielsweise von &lt;a href="https://www.greengeeks.com/blog/set-up-woocommerce-wordpress-ultimate-guide/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;GeeksForGeeks&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; oder &lt;a href="https://themeisle.com/blog/how-to-set-up-woocommerce/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Themeisle&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;), also verzichte ich im Folgenden auf eine eigene Darstellung.&lt;/p&gt;&#10;&lt;p&gt;Nach dem Querlesen einiger Anleitungen fasse ich zusammen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Ein WooCommerce-Shop lässt sich einfach per Assistent aufsetzen und konfigurieren&lt;/li&gt;&#10;&lt;li&gt;Es stehen sehr viele Themes zur Verfügung und einfach einzubindende Shopdesigns bieten für jeden Geschmack eine passende Optik&lt;/li&gt;&#10;&lt;li&gt;Es hilft, bereits eine Wordpress-Seite zu haben und etwas Basiswissen bezüglich Domains, DNS und Hostern zu besitzen&lt;/li&gt;&#10;&lt;li&gt;Viele Zahlungsdienstleister und einige Versandabwickler sind vorkonfiguriert auswählbar&lt;/li&gt;&#10;&lt;li&gt;Es gibt eine Menge YouTube-Lehrvideos zum Thema und eine ausführliche Dokumentation liegt ebenfalls vor&lt;/li&gt;&#10;&lt;li&gt;Zahllose Plugins erlauben die Optimierung des Shops auch auf sehr spezialisierte Gebiete und Einsatzzwecke&lt;/li&gt;&#10;&lt;li&gt;Allerdings steht dieser Optionsvielfalt gegenüber, dass die Auswahl der &amp;ldquo;besten Lösung&amp;rdquo; schwer fallen kann und dass Optimierungen der Seite hinsichtlich Geschwindigkeit und Auffindbarkeit komplex werden können&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Alles in allem wirkt WooCommerce auf mich wie eine ausgewogene Mischung aus &amp;ldquo;Ich will schnell etwas Brauchbares zusammen klicken&amp;rdquo; und &amp;ldquo;Ich möchte alles konfigurieren und umbauen können&amp;rdquo;.&lt;/p&gt;&#10;&lt;h3 id="snipcart"&gt;Snipcart&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://snipcart.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Snipcart&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; würde ich als &amp;ldquo;Shopping-Plugin&amp;rdquo; für eine beliebige Website verstehen. Auch statische Websites, die eigentlich nur einen Warenkatalog darstellen können, werden mit Snipcart zu einer vollwertigen eCommerce-Lösung. Kernelement ist hier der Warenkorb &amp;ldquo;cart&amp;rdquo;, über den der Kaufvorgang abgewickelt wird.&lt;/p&gt;&#10;&lt;p&gt;Die Schnittstelle ist das &lt;code&gt;HTML Markup&lt;/code&gt; der Webseite. Snipcart wird als &lt;code&gt;JavaScript&lt;/code&gt;-Blob eingebunden und per &lt;code&gt;HTML&lt;/code&gt; referenziert. Gestaltung und Aussehen läuft über cascading style sheets. In meinem Fall also könnte ich das vorhandene &amp;ldquo;Tech Stack&amp;rdquo; bestehend aus einer statischen Jekyll-site mit CD-Pipeline in Gitea auf einer Caddyserver-Instanz auch für den Shop nutzen und mit minimalen Anpassungen eine Optik aus einem Guss für Blog und Shop zaubern. Klingt doch schon einmal prima.&lt;/p&gt;&#10;&lt;p&gt;Snipcart wird von einer kanadischen Firma in Québec entwickelt. Es bezeichnet sich als &amp;ldquo;&lt;a href="https://snipcart.com/ecommerce-inventory-management" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Shopping Cart Platform&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;rdquo;. Für die Installation wird ein Account benötigt und man muss die eigene Shopping-Domain ähnlich wie bei der Google Search Console oder Google Analytics im Web-Frontend von Snipcart angeben, um einen sogenannten &lt;code&gt;API-Key&lt;/code&gt; zu erhalten. Nur mit diesem wird der JavaScript-Blob später auch korrekt funktionieren.&lt;/p&gt;&#10;&lt;p&gt;Und damit kommt das erste größere Problem: Snipcart schränkt meine Freiheit ein.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;preconnect&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;https://app.snipcart.com&amp;gt;&amp;#34;&lt;/span&gt; /&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;preconnect&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;https://cdn.snipcart.com&amp;gt;&amp;#34;&lt;/span&gt; /&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mir zeigt dies, dass Snipcart die Daten gern &lt;a href="https://snipcart.com/blog/add-shopping-cart-to-website" target="_blank" rel="noopener noreferrer" class="external-link"&gt;bei sich haben will&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Ich muss deren content delivery network für die Abwicklung des Bestellprozesses verwenden und bei mir einbinden. Es ist also mitnichten so, dass alle Verbindungen bei mir bleiben und ich den JS-Code von Snipcart &amp;ldquo;kaufe&amp;rdquo; und im Browser des Kunden bzw. eine dahinter liegende Datenbanklösung auf meinem Server betreibe.&lt;/p&gt;&#10;&lt;p&gt;Wenn einem also volle Autonomie und Kontrolle über die eigene Site nicht super wichtig ist, kann Snipcart eine sehr elegante Shopsystem-Lösung sein. Dies lohnt sich vor allem für bereits bestehende Seiten. Für mich jedoch ist sie damit raus.&lt;/p&gt;&#10;&lt;h3 id="opencart"&gt;OpenCart&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://www.opencart.com/index.php?route=common/home" target="_blank" rel="noopener noreferrer" class="external-link"&gt;OpenCart&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bietet eine &lt;code&gt;PHP&lt;/code&gt;-basiertes Shopmodul kostenlos und quelloffen an. Es kann sowohl als &amp;ldquo;Software as a Service&amp;rdquo; über Drittanbieter gebucht wie auch per &lt;a href="https://github.com/opencart/opencart/blob/master/INSTALL.md" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; heruntergeladen und auf eigenen Servern bereitgestellt werden.&lt;/p&gt;&#10;&lt;p&gt;Installation und Administration geschehen über eine Weboberfläche und ist meines Ermessens nach unkompliziert möglich. Auch im DockerHub findet man ein paar Images, mit welchen man direkt loslegen kann.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-05-15-opencart.avif" alt="Image: OpenCart demo, category overview"&gt;&lt;/figure&gt;&#10;&lt;p&gt;OpenCart ist vollständig an die eigenen Bedürfnisse anpassbar. Auch vorgefertigte Themes können eingebunden werden. Aus Neugier habe ich mal ein paar Webseiten angeschaut, die OpenCart einsetzen: Das Shopsystem ist aus meiner Sicht gut strukturiert und einfach zu bedienen. Jedoch ist die Ladegeschwindigkeit der von mir besuchten Beispielseiten nicht immer super schnell.&lt;/p&gt;&#10;&lt;p&gt;Im Gesamtpaket halte ich OpenCart mit seiner anpassbaren, leichtgewichtigen Shoplösung in &lt;code&gt;PHP&lt;/code&gt; auch durch die immer noch aktive Community (13+ Jahre!) für eine gute Lösung.&lt;/p&gt;&#10;&lt;h3 id="django"&gt;Django&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://www.djangoproject.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Django&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist ein Webframework für &lt;code&gt;Python&lt;/code&gt;. Es bietet viele Funktionen, ermöglicht Erweiterungen durch schlichtes Einbinden von Python-Modulen und man kann auch als Programmier-Einsteiger schnell funktionierende Lösungen erzeugen. Obwohl es eine Menge Bibliotheken beispielsweise für Websites, Shops &lt;a href="https://django-shop.readthedocs.io/en/latest/tutorial/intro.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;wie z.B. django-SHOP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, Datenbankeinbindungen, Einbindung von Zahlsystemen usw. gibt, muss alles noch selbst zusammen gesteckt und konfiguriert werden.&lt;/p&gt;&#10;&lt;p&gt;Dadurch erhält man zwar volle Kontrolle und eine perfekt an die eigenen Bedürfnisse anpassbare Lösung - doch es gibt auch einen gewichtigen Nachteil: Lernzeit. Die Sicherheit der Datenbank muss selbst hergestellt werden, Validierung von User inputs muss selbst programmiert oder zumindest korrekt konfiguriert werden. Das Abspeichern von Kundenstammdaten passiert standardmäßig erst einmal im Klartext. Zugriffs- und Löschkonzepte müssen selbst erstellt und implementiert werden bzw. man sollte Zeit einplanen, passende Module ausfindig zu machen und zu integrieren.&lt;/p&gt;&#10;&lt;p&gt;Sollen Frameworks verwendet werden, empfiehlt es sich vorab eine aktive Wartung durch die Community zu prüfen. Für mich sieht es beispielsweise so aus, als wäre bei &lt;a href="https://github.com/awesto/django-shop" target="_blank" rel="noopener noreferrer" class="external-link"&gt;django-SHOP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bereits seit dem Jahre 2021 keine große Bewegung mehr auf dem Github-Repo. Eine kurze Suche hingegen offenbart &lt;a href="https://github.com/topics/django-ecommerce" target="_blank" rel="noopener noreferrer" class="external-link"&gt;eine Menge Alternativen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; - hier gilt es nun, etwas Passendes ins Auge zu fassen oder gleich selbst das Rad neu zu erfinden 😃&lt;/p&gt;&#10;&lt;h2 id="die-alternative-für-anfänger-wie-mich-ohne-shopsystem"&gt;Die Alternative für Anfänger wie mich: ohne Shopsystem&lt;/h2&gt;&#10;&lt;p&gt;Was ist mit &amp;ldquo;Ohne Shopsystem&amp;rdquo; gemeint? Ein formularbasierter Ansatz. Hier schaut sich der User eine statische Website an, die Katalog- und Produktseiten enthält. Zusätzlich sind ein paar Elemente eingeblendet wie ein Dropdown-Menü für Variante und ein Feld für die Stückzahl. Daneben ein schlichter Button. Wird dieser geklickt, so öffnet sich ein Formular, welches automatisch die geklickten Inhalte überträgt und nach Kontaktdaten des Users fragt.&lt;/p&gt;&#10;&lt;p&gt;Wird alles ordnungsgemäß ausgefüllt, so erzeugt das Formular beim Absenden eine PGP-verschlüsselte Mail an mich. Ich kann dann die Order analog zu manuell erstellten Bestell-Mails sichten, eine Bestellbestätigung senden und die Zahlung anfordern.&lt;/p&gt;&#10;&lt;p&gt;So lässt sich mit wenigen Produkten bei wenigen Kunden ohne großen Aufwand ein Shop-Äquivalent datensparsam umsetzen. Natürlich nicht so bequem wie mit einer Ecommerce-Lösung. Aber für den Anfang reicht das vielleicht trotzdem.&lt;/p&gt;&#10;&lt;h2 id="im-nächsten-artikel"&gt;Im nächsten Artikel&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/ecommerce-selection/"&gt;Auswahl und Implementierung&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>Webshop selber aufsetzen - Teil1</title><link>https://blog.schallbert.de/ecommerce-requirements/</link><pubDate>Tue, 15 Apr 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/ecommerce-requirements/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-04-15-ecommerce-pt1-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: text saying &amp;#39;&amp;#39;E-commerce Pt1: Requirements of a technophiliac&amp;#39;&amp;#39;"&#10; title="Webshop selber aufsetzen - Teil1" /&gt;&#10;&lt;p&gt;Dies ist Teil eins der Artikelserie über den Aufbau meines eigenen Webshops. Ich liste Anforderungen und Wünsche für meine Ecommerce-Lösung auf und grenze mich damit auf ein paar passende Software-Lösungen ein, die ich dann für meinen Webshop verwenden könnte.&lt;/p&gt;&#10;&lt;h2 id="was-mein-webshop-können-muss"&gt;Was mein Webshop können muss&lt;/h2&gt;&#10;&lt;p&gt;Für mich als Betreiber soll der Webshop möglichst schlank, transparent und automatisiert operieren. Ich möchte nur wenig Zeit mit Betrieb, Wartung und Pflege verbringen. Zahlungsabwicklung, Verwaltung des Bestellstatus, Kundenregistrierung, Versenden von Kundeninformation sowie eine große Anzahl Schritte im Versandprozess wie Frankieren und Einpflegen von Versandbestätigungen soll automatisiert stattfinden oder zumindest für die Automatisierung vorbereitet werden.&lt;/p&gt;&#10;&lt;p&gt;Aber auch für Kunden muss der Webshop verständlich aufgebaut sein: Simpel und mit wenigen Klicks von der Startseite bis zum Checkout, leicht zu bedienen, ähnlich zu anderen gängigen Webshops, mit unkomplizierter Zahlungsabwicklung. Dabei sollen so wenige Daten vom Kunden wie möglich erhoben werden.&lt;/p&gt;&#10;&lt;p&gt;Der Webshop muss nur wenige Produkte darstellen. Daher kann die Navigation sehr einfach gehalten werden. Durch die geringe Anzahl Produktkategorien kan möglicherweise komplett auf ein Baumsystem verzichtet werden.&lt;/p&gt;&#10;&lt;p&gt;Schauen wir uns mal schnell im Detail an, welche Funktionen der Webshop am Ende bereitstellen muss.&lt;/p&gt;&#10;&lt;h3 id="funktionen-für-admins"&gt;Funktionen für Admins&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Produkt anlegen&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Titel&lt;/li&gt;&#10;&lt;li&gt;Tags&lt;/li&gt;&#10;&lt;li&gt;Kurzbeschreibung&lt;/li&gt;&#10;&lt;li&gt;Beschreibung&lt;/li&gt;&#10;&lt;li&gt;Bilder&lt;/li&gt;&#10;&lt;li&gt;Videos&lt;/li&gt;&#10;&lt;li&gt;Preis&lt;/li&gt;&#10;&lt;li&gt;Steckbrief / Datenblatt / Anleitung&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ol start="2"&gt;&#10;&lt;li&gt;Produkt editieren&lt;/li&gt;&#10;&lt;li&gt;Produkt entfernen&lt;/li&gt;&#10;&lt;li&gt;Bestellungen verwalten&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schritt quittieren&lt;/li&gt;&#10;&lt;li&gt;Schritt erneut starten&lt;/li&gt;&#10;&lt;li&gt;Prozess stornieren&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ol start="5"&gt;&#10;&lt;li&gt;Kunden verwalten&#10;&lt;ul&gt;&#10;&lt;li&gt;anlegen (sandbox)&lt;/li&gt;&#10;&lt;li&gt;löschen (Datenschutz)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="funktionen-für-käufer"&gt;Funktionen für Käufer&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Webshop durchstöbern&lt;/li&gt;&#10;&lt;li&gt;Produkt(e) konfigurieren oder Produktvariante auswählen (z.B. Farbe, Qualität)&lt;/li&gt;&#10;&lt;li&gt;Produkt(e) in einen Warenkorb legen&lt;/li&gt;&#10;&lt;li&gt;Warenkorb verwalten&lt;/li&gt;&#10;&lt;li&gt;Registrierung vornehmen (optional, nur zur Kundendatenablage und Bestellverwaltung)&lt;/li&gt;&#10;&lt;li&gt;Versand wählen&lt;/li&gt;&#10;&lt;li&gt;Zahlung vornehmen&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="automatische-funktionen"&gt;Automatische Funktionen&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Zahlungsregistrierung&lt;/li&gt;&#10;&lt;li&gt;Rechnungserstellung und -Versand&lt;/li&gt;&#10;&lt;li&gt;Inventarverwaltung&lt;/li&gt;&#10;&lt;li&gt;Versandbenachrichtigung&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="nichtfunktionale-anforderungen"&gt;Nichtfunktionale Anforderungen&lt;/h2&gt;&#10;&lt;p&gt;Hier liste ich auf, was der Webshop &amp;ldquo;nebenbei&amp;rdquo; noch so alles können soll. Alles, was nicht unmittelbar zur Auftragsabwicklung benötigt oder für Kunden sichtbar wird.&lt;/p&gt;&#10;&lt;h3 id="usability-betreiberseite"&gt;Usability Betreiberseite&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Der Shop soll mit möglichst geringen Abhängigkeiten zu Software Dritter auskommen und nur diejenigen Module einbinden, die unbedingt benötigt werden. Beispiele:&#10;&lt;ul&gt;&#10;&lt;li&gt;Zahlungsabwicklung&lt;/li&gt;&#10;&lt;li&gt;Warenkorb&lt;/li&gt;&#10;&lt;li&gt;Kundenkonto / Adressdaten&lt;/li&gt;&#10;&lt;li&gt;Lagerbestände&lt;/li&gt;&#10;&lt;li&gt;Bestellabwicklung (Mailer) für Käufer&lt;/li&gt;&#10;&lt;li&gt;Rechnungsstellung.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Der Shop soll nur so viele dynamisch erzeugte Elemente enthalten wie unbedingt nötig.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="usability-käuferseite"&gt;Usability Käuferseite&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Der Webshop soll sich wie aktuelle übliche ECommerce-Lösungen anfühlen: Flink, übersichtlich, einfach.&lt;/li&gt;&#10;&lt;li&gt;Kein Zwang zum Kundenkonto.&lt;/li&gt;&#10;&lt;li&gt;Der Shop soll gängige Zahlungsmöglichkeiten unterstützen (Debit, Kredit, PayPal, Apple/GooglePay etc.). Dabei sind nur lokale (in Deutschland ansässige) Zahlungsdienstleister akzeptabel.&lt;/li&gt;&#10;&lt;li&gt;Für den User sollen Lagerbestände bzw. Fertigungszeiten transparent angezeigt werden.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="datenschutz"&gt;Datenschutz&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Der Shop soll nur für die Geschäftsabwicklung notwendige Daten erfassen.&lt;/li&gt;&#10;&lt;li&gt;Falls technisch und rechtlich möglich soll der Shop ohne Cookie-Banner auskommen können.&lt;/li&gt;&#10;&lt;li&gt;Kundenkonten sollen automatisch nach 2 Jahren der Nicht-Nutzung (mit Ankündigung 2 Wochen vorher) gelöscht werden.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="look--feel"&gt;Look &amp;amp; Feel&lt;/h3&gt;&#10;&lt;p&gt;Hier einige Beispiele für Shopdesigns:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://store.caddyserver.com/en-eur/collections/all" target="_blank" rel="noopener noreferrer" class="external-link"&gt;caddyshop&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://dev.to/contentful_blog/how-to-build-an-ecommerce-static-site-with-jekyll-contentful-and-commerce-layer-3c9" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Beispiel-ecommerce mit Jekyll&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.geeksforgeeks.org/e-commerce-website-using-django/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Beispiel-ecommerce mit Django&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Davon abgeleitete Anforderungen:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Der Webshop soll von Aufbau und Farbgebung her an meine Blogseite erinnern und eine einheitliche Optik bieten.&lt;/li&gt;&#10;&lt;li&gt;Es sollen nur statische Fonts ohne Nachladen verwendet werden.&lt;/li&gt;&#10;&lt;li&gt;Produktbeschreibungen sollen als Markdown-Dokumente (oder Ähnlich) hochgeladen werden können.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="datenbank--oder-dateilösung"&gt;Datenbank- oder Dateilösung?&lt;/h2&gt;&#10;&lt;p&gt;Es muss nicht immer gleich eine Datenbank sein. Für viele Anwendungen werden nur Bruchteile der Funktionalität gängiger Datenbanklösungen benötigt, doch erhöht sich die Komplexität des Gesamtsystems erheblich. Sollte z.B. gleichzeitiges Schreiben selten oder nie vorkommen und keine starke Verkettung von Daten vorliegen, so kann eine Dateilösung einfacher, schneller und effizienter sein &lt;a href="https://engineeringkiosk.dev/podcast/episode/129-simplify-your-stack-files-statt-datenbanken/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Link zu einer passenden Engineering Kiosk Podcast-Episode&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Andererseits kommen viele Shopsysteme &amp;ldquo;with batteries included&amp;rdquo;, verfügen also schon von sich aus über eingebaute und für die Programmierer nahezu transparente Datenverwaltung.&lt;/p&gt;&#10;&lt;h3 id="dateilösung-statische-website"&gt;Dateilösung (statische Website)&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Auf Betreiberseite soll sich der Webshop so anfühlen wie ein Git-Projekt (everything is a file).&lt;/li&gt;&#10;&lt;li&gt;Änderungen im Angebot, Artikelbeschreibung, Preise etc. sollen per &lt;code&gt;git push&lt;/code&gt; auf den Server gespielt werden können - wie bei einer statischen Webseite.&lt;/li&gt;&#10;&lt;li&gt;Wird ein Pull Request nach &lt;code&gt;main&lt;/code&gt; gemerged, so soll der Webshop neu bauen (CD-Pipeline) und die Änderungen auf dem Server veröffentlichen.&lt;/li&gt;&#10;&lt;li&gt;Für Inventarverwaltung, Warenkorb, Kundeninteraktion und automatisierte Funktionen sind dynamische Elemente erforderlich. Hier gilt zu entscheiden, was direkt in Javascript und welche Elemente besser per Automatisierungspipeline gelöst werden sollen.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="datenbanklösung-dynamische-website"&gt;Datenbanklösung (dynamische Website)&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Produkte werden in der Datenbank gehalten.&lt;/li&gt;&#10;&lt;li&gt;Die Verwaltung des Webshops geschieht per Web-Oberfläche.&lt;/li&gt;&#10;&lt;li&gt;Die Datenbankverwaltung soll teilautomatisiert erfolgen, dabei sind Backups und Verschlüsselung für Kundendaten vorzusehen.&lt;/li&gt;&#10;&lt;li&gt;Die Datenbank ist transparent für den Admin: Alle Einträge hier erfolgen über eine Web-Schnittstelle.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="im-nächsten-artikel"&gt;Im nächsten Artikel&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/ecommerce-alternatives/"&gt;Vergleich verschiedener Ecommerce-Lösungen&lt;/a&gt; für meinen Webshop&lt;/p&gt;&#10;</description></item><item><title>PeerTube im Fediverse</title><link>https://blog.schallbert.de/fediverse-ups-and-downs/</link><pubDate>Tue, 01 Apr 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/fediverse-ups-and-downs/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/announcements/2025-03-17-makertube.jpg"&#10; class="post-cover"&#10; alt="Image: My MakerTube channel &amp;#39;&amp;#39;CNC corner&amp;#39;&amp;#39;"&#10; title="PeerTube im Fediverse" /&gt;&#10;&lt;h2 id="kurze-begriffsklärung"&gt;Kurze Begriffsklärung&lt;/h2&gt;&#10;&lt;h3 id="fediverse"&gt;Fediverse?&lt;/h3&gt;&#10;&lt;p&gt;Das &lt;a href="https://fediverse.info/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Fediverse nennt etwas Fediverse&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, wenn mehrere durchaus unterschiedliche Anbieter dasselbe Protokoll nutzen, um Nutzer dieser Anbieter frei miteinander interagieren zu lassen - und das in Bezug auf digitale Medien. Somit entsteht stark vernetzte aber dabei stets dezentrale Dienste, bei dem freie Anbieterwahl zum Konzept gehört. &lt;a href="https://www.theverge.com/24063290/fediverse-explained-activitypub-social-media-open-protocol" target="_blank" rel="noopener noreferrer" class="external-link"&gt;The Verge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; geht hier einmal ins Detail und gibt einen Ausblick.&lt;/p&gt;&#10;&lt;h3 id="peertube"&gt;PeerTube?&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/PeerTube" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PeerTube&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist eine dezentrale Videoplattform, bei der sich mehrere Server die Last teilen und auch Zuschauer beim Verbreiten von Videos helfen können. Grob gesagt steht &lt;em&gt;PeerTube&lt;/em&gt; zu &lt;em&gt;YouTube&lt;/em&gt; wie &lt;em&gt;Mastodon&lt;/em&gt; zum Kurznachrichtendienst &lt;em&gt;X&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h2 id="der-anlass"&gt;Der Anlass&lt;/h2&gt;&#10;&lt;p&gt;Dieser Artikel beginnt mit einer E-Mail.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;You&amp;rsquo;re receiving this non-automated email because you&amp;rsquo;re one of the handful of people who has a video hosted on this PeerTube instance. I have decided to shut down this service [&amp;hellip;]&amp;rdquo; - Tom, &lt;em&gt;scitech.video&lt;/em&gt; administrator&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Es tue ihm wirklich leid, aber der Weiterbetrieb der Instanz koste zu viel Zeit, Wartungsaufwand und Geld. Zudem sei die Plattform im Moment nur wenig genutzt, sodass der Aufwand unverhältnismäßig erscheine.&lt;/p&gt;&#10;&lt;p&gt;So weit, so verständlich.&lt;/p&gt;&#10;&lt;h2 id="toms-geschichte-scitechvideo"&gt;Toms Geschichte: &lt;em&gt;scitech.video&lt;/em&gt;&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-04-01-scitechvideo-stats.avif" alt="Image: scitech.video statistics as of Mid-March 2025: ~16k views total"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Da ich dort allerdings alle meine Videos hochgeladen und verlinkt hatte, war ich sehr an den Hintergründen interessiert. Also hakte ich nach. Meine Nachfrage wurde ausführlich und prompt beantwortet.&lt;/p&gt;&#10;&lt;h3 id="die-anfänge"&gt;Die Anfänge&lt;/h3&gt;&#10;&lt;p&gt;Tom setzte seine PeerTube-Instanz im Jahr 2019 auf. Ursprünglich, um eigene Inhalte zu teilen. Bereits wenig später öffnete er das Hosting-Angebot, wenn auch mit leichtem Unwohlsein, für andere.&lt;/p&gt;&#10;&lt;p&gt;Aus der Admin-Perspektive wirke so eine Videoplattform wie der &amp;ldquo;wilde Westen&amp;rdquo;. Seinem Angebot ein gesundes Wachstum angedeihen zu lassen, stellte sich als recht schwierig heraus - vor allem, weil viele Bots und Werbetreibende unterwegs sind.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Growing the service turned out to be challenging. Every time I opened registrations spambots started creating accounts easily and uploading videos with advertisements in the video descriptions [&amp;hellip;]&amp;rdquo; - Tom, &lt;em&gt;scitech.video&lt;/em&gt; administrator, via E-Mail&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="viele-bots-und-unerwünschte-user-unterwegs"&gt;Viele Bots und unerwünschte User unterwegs&lt;/h3&gt;&#10;&lt;p&gt;Diese unerwünschten Spammer, Scammer, Advertiser konnte er zwar stets davon abhalten, &lt;em&gt;scitech.video&lt;/em&gt; zu verseuchen. Dennoch bedeutete dies ständige Arbeit und manuelle Überwachung der automatischen Tools zum Ablehnen und Moderieren.&lt;/p&gt;&#10;&lt;p&gt;An dieser Stelle möchte ich Tom beipflichten. Ich habe im Schnitt etwa &lt;em&gt;340&lt;/em&gt; fehlgeschlagene und dann von mir weggeblockte Verbindungen externer IP-Adressen auf meinen Server pro Tag. Allein auf dem &lt;code&gt;ssh-Port&lt;/code&gt;. Und mein Server ist weder besonders groß, bekannt oder kann als lohnenswertes Ziel durchgehen. Ganz zu schweigen von zahllosen Versuchen, auf meine Gitea-Nutzeroberfläche zu kommen, die Website nach verborgenen Links zu crawlen und vieles mehr.&lt;/p&gt;&#10;&lt;p&gt;Für Betreiber eines Portals, welches auch externen Usern Schreibzugriff gibt (Videos, Beschreibungen, Tags, Kommentare), potenziert sich das Problem natürlich nochmal. Hier gehört anders als bei meiner Seite ja zum Konzept, sich zu beteiligen.&lt;/p&gt;&#10;&lt;h3 id="copyright"&gt;Copyright?&lt;/h3&gt;&#10;&lt;p&gt;Genau, grundsätzlich fällt aus Sicht des Plattformbetreibers erst einmal jedes Video unter das Urheberrecht oder verwandte Schutzrechte. Aber ist der Nutzer, der gerade dieses Video veröffentlichen will auch der Urheber? Oder hat dieser Jemand Nutzungsrechte erworben?&lt;/p&gt;&#10;&lt;p&gt;Je nach Land und zugehöriger Rechtslage müssen sich Admins mit diesem Thema sehr genau auseinandersetzen. Es können hohe Strafen sowohl für die Betreiber als auch für die User fällig werden, sollte eine Urheberrechtsverletzung vorliegen.&lt;/p&gt;&#10;&lt;p&gt;Ersteres ist zumindest in Deutschland einem &lt;a href="https://datenbank.nwb.de/Dokument/1055605/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Urteil des Bundesgerichtshofes vom 23.10.2024&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, wie folgt aufzulösen: &amp;ldquo;[&amp;hellip; sind] nach einem klaren Hinweis auf eine Rechtsverletzung die dort eingestellten Angebote im Rahmen des technisch und wirtschaftlich Zumutbaren auf gleichartige Verletzungen zu überprüfen und rechtsverletzende Inhalte zu sperren oder zu löschen&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;Durch die starke Fokussierung der Instanzen auf bestimmte Themenbereiche fallen Verletzungen jedoch meist schnell auf - denn auch User können Beiträge &amp;ldquo;reporten&amp;rdquo;.&lt;/p&gt;&#10;&lt;h3 id="die-rechtslage-für-soziale-netzwerke"&gt;Die Rechtslage für soziale Netzwerke&lt;/h3&gt;&#10;&lt;p&gt;Einige Länder bereiten im Moment Gesetze vor, die Kinder und Jugendliche vor der Nutzung sozialer Netzwerke und damit verbundenen Gefahren schützen soll. Als Referenz hier ein &lt;a href="https://www.bzkj.de/bzkj/service/alle-meldungen/bzkj-schutzmassnahmen-und-staerkere-medienbildung-fuer-kinder-und-jugendliche-257566" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Link zur Bundeszentrale für Jugend- und Medienschutz&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Auch hier sind die Admins gefragt, diese Regeln umzusetzen - doch wie?&lt;/p&gt;&#10;&lt;p&gt;Dies jedenfalls fragt sich Tom angesichts der zunehmend vielfältiger werdenden Rechtsauffassungen unterschiedlicher Länder.&lt;/p&gt;&#10;&lt;h3 id="updates-und-wartung"&gt;Updates und Wartung&lt;/h3&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;PeerTube upgrades were a bit of a drag because everything was tightly coupled with their specific nginx configuration, with breaking changes that had to be adjusted every couple of minor versions.&amp;rdquo; - Tom, &lt;em&gt;scitech.video&lt;/em&gt; administrator, via E-Mail&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Die Arbeiten seien nicht besonders schwierig auszuführen, dennoch kosten sie eine Menge Zeit. Wie bei anderer Software auch könne es passieren, dass ein PeerTube-Server zum Beispiel nach Betriebssystemupdates sich nicht mehr hochfahren lässt oder es zu Inkompatibilitäten verschiedener Softwarestände oder Modulen untereinander kommt.&lt;/p&gt;&#10;&lt;p&gt;Aus Leistungsgründen stellt sich das Testen als schwierig heraus, sodass oft direkt in &amp;ldquo;Prod&amp;rdquo; gearbeitet wird. Im Fehlerfalle leidet der Livebetrieb.&lt;/p&gt;&#10;&lt;p&gt;Die Konfiguration sei relativ aufwendig und müsse auf die Hardware angepasst werden. Größere PeerTube-Updates erfordern auch hier Änderungen.&lt;/p&gt;&#10;&lt;h3 id="fazit"&gt;Fazit&lt;/h3&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;I&amp;rsquo;m also disillusioned about the fediverse generally as a communication medium. A constant battle against spam/LLMs, so much anger, so much focus on follows, likes and boosts.&amp;rdquo; - Tom, &lt;em&gt;scitech.video&lt;/em&gt; administrator, via E-Mail&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Tom ist frustriert, dass auch hier die Aufmerksamkeitsökonomie durchschlägt. Obwohl werbefrei gedacht und als kleine Graswurzelbewegung ausgerollt, werden aus Idealismus heraus erstellte Instanzen schnell für Werbung und Selbstdarstellung missbraucht. Die Kosten beliefen sich zudem auf mehrere hundert Dollar im Jahr, was die Kosten-Nutzen-Bilanz weiter verschlechtere.&lt;/p&gt;&#10;&lt;p&gt;Eine Lösung könne hier &amp;ldquo;die Rückkehr zu Web1.0 oder geschlossene Gemeinschaften&amp;rdquo; darstellen, wie es sie zum Beispiel bei &lt;em&gt;Meatspace&lt;/em&gt;, &lt;em&gt;Matrix&lt;/em&gt;, &lt;em&gt;Discord&lt;/em&gt; etc. bereits gebe.&lt;/p&gt;&#10;&lt;p&gt;Auch alle anderen Faktoren machen Tom den Weiterbetrieb seiner Instanz schwer: Die Nutzerbasis ist nie besonders groß geworden und es gibt nur wenige Menschen, die ihre Inhalte hier noch hochladen. Als Vater hat er nun viel weniger Zeit zur Verfügung und muss Projekte priorisieren. Somit beendet er nun seine immerhin sechs Jahre betriebene Reise als Anbieter im Fediverse.&lt;/p&gt;&#10;&lt;p&gt;Ich wünsche Tom für die Zukunft weiterhin viel Kraft, Idealismus und Enthusiasmus.&lt;/p&gt;&#10;&lt;h2 id="jans-geschichte-makertubenet"&gt;Jans Geschichte: &lt;em&gt;makertube.net&lt;/em&gt;&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-04-01-makertube-stats.avif" alt="Image: MakerTube statistics as of Mid March-2025: ~120k views total"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Da ich nun nach einer neuen Hosting-Plattform für meine Videos Ausschau halten musste, bemühte ich die Suche für &amp;ldquo;video-maker&amp;rdquo; auf der &lt;a href="https://joinpeertube.org/instances?profile=video-maker" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PeerTube-Website&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Da meine Inhalte einen klaren Tech-Fokus haben und von Forschung, Produktentwicklung bis in den Vorserienbau reichen, war es nicht schwer, schnell passende Kandidaten ausfindig zu machen.&lt;/p&gt;&#10;&lt;p&gt;So kam ich dann zu meinem &lt;a href="https://makertube.net/c/cnc_corner/videos" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Kanal auf MakerTube&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Nach erfolgreicher Anmeldung schrieb ich eine Mail an die Admins mit ähnlichen Fragen, die ich Tom bereits gestellt hatte.&lt;/p&gt;&#10;&lt;h3 id="motivation---warum-makertube"&gt;Motivation - warum MakerTube?&lt;/h3&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Ich habe einen kleinen Youtube-Kanal und die permanente Werbung und der Fokus auf die großen Kanäle, Klickbait etc. und wie die Leute gezwungen werden sich der Content-Maschine anzupassen ging mir auf die Nerven. [&amp;hellip;]&amp;rdquo; - Jan, &lt;em&gt;MakerTube&lt;/em&gt; administrator, via E-Mail&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Jan war bereits im Fediverse unterwegs und beschäftigt sich auch beruflich mit Webdiensten, die stark skalieren müssen. Daher erschien es für ihn logisch, selbst eine Instanz aufzusetzen - dann aber natürlich mit den wirklich interessanten Themen dieser Welt. Es sei ja nicht als Konkurrenz zu YouTube gedacht und fokussiere sich daher auf Maker, Musikerinnen, Künstler etc.&lt;/p&gt;&#10;&lt;h3 id="ist-der-einstieg-leicht"&gt;Ist der Einstieg leicht?&lt;/h3&gt;&#10;&lt;p&gt;Es bedeute schon einiges an Aufwand, so eine Instanz hochzuziehen und performant zu halten. &lt;a href="https://framasoft.org/en/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Framasoft&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, welche die PeerTube-Software entwickeln, machten einen Superjob und unterstützen die Hoster-Community wo sie können.&lt;/p&gt;&#10;&lt;p&gt;Ich persönlich kann bestätigen, dass sich PeerTube in den neueren Versionen ganz anders anfühlt als noch vor ein paar Jahren und sich mit praktischen Features wie automatisch generierten Untertiteln immer näher an den Funktionsumfang von &lt;em&gt;YouTube&lt;/em&gt; heranrobbt.&lt;/p&gt;&#10;&lt;p&gt;Das Fediverse, so Jan, sei nach wie vor eine Nische. Daher falle das Wachstum seiner Instanz eher moderat und sehr organisch aus. &lt;em&gt;PeerTube&lt;/em&gt; sei im Moment noch etwas für enthusiastische Videomacher; am normalen Publikum laufen die Inhalte größtenteils ungesehen vorbei.&lt;/p&gt;&#10;&lt;h3 id="wie-groß-ist-der-zeitaufwand"&gt;Wie groß ist der Zeitaufwand?&lt;/h3&gt;&#10;&lt;p&gt;Jan legte den Fokus auf niedrige laufende Kosten bei gleichzeitiger Skalierbarkeit, falls die Sache plötzlich doch groß werden sollte. Jetzt, da das System reibungsfrei läuft, liege der Aufwand bei ein paar Stunden pro Woche für Moderation von Usern, Inhalten und Support.&lt;/p&gt;&#10;&lt;p&gt;Ausnahmen: Größere Updates, Upgrades und technische Probleme.&lt;/p&gt;&#10;&lt;h3 id="was-sagen-die-leute"&gt;Was sagen die Leute?&lt;/h3&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Es gibt positive Rückmeldungen und die Leute sind dankbar, dass ich die Seite betreibe und ich mir die Arbeit mache, aber ich würde sagen nicht sehr viele.&amp;rdquo; - Jan, &lt;em&gt;MakerTube&lt;/em&gt; administrator, via E-Mail&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Aber natürlich gebe es auch immer mal wieder Entrüstung darüber, dass das halt nicht YouTube wäre und nicht jeder sofort einen Account aufmachen könne.&lt;/p&gt;&#10;&lt;h3 id="wie-erkennt-ihr-urheberrechtsverletzungen"&gt;Wie erkennt ihr Urheberrechtsverletzungen?&lt;/h3&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Bezüglich Copyright gibt es da keine direkten Hilfsmittel. Es gibt die Report-Funktion an den Videos, um auf Verstöße gegen Copyright oder Instanzregeln hinzuweisen. Das wird auch von Benutzern genutzt.&amp;rdquo; - Jan, &lt;em&gt;MakerTube&lt;/em&gt; administrator, via E-Mail&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Verantwortung und Haftung lägen bei so kleinen Anbietern wie &lt;em&gt;MakerTube&lt;/em&gt; in erster Linie beim User. Wenn allerdings auf Verstöße hingewiesen wird, müssen die Admins tätig werden und Inhalte löschen. Auf &lt;em&gt;MakerTube&lt;/em&gt; sei dies allerdings im Alltag kein Problem: Die Registrierungen neuer User wird genau geprüft und im Zweifel werde nachgefragt, um welche Inhalte es gehe. Bei den meisten Verletzungen gehe es um Hintergrundmusik der Videos, da gebe es scheinbar weniger Bewusstsein für Urheberrecht wie bei Uploads von Videokopien.&lt;/p&gt;&#10;&lt;h3 id="und-wie-finanziert-sich-das-ganze"&gt;Und wie finanziert sich das Ganze?&lt;/h3&gt;&#10;&lt;p&gt;MakerTube finanziert sich über &lt;a href="https://makertube.net/about/instance/support" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Spenden&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Gerade zu Anfang ging laut Jan viel Zeit dafür drauf, dies den Leuten klar zu machen und genügend Budget für einen flüssigen Betrieb zu erhalten. Eine Videoplattform sei ressourcenintensiv: Viel CPU-Power für Transkodierung und Streaming, einen Haufen Plattenplatz für die ganzen Assets bei gleichzeitig hohen Anforderungen an die Bandbreite, gerade in &amp;ldquo;Stoßzeiten&amp;rdquo;.&lt;/p&gt;&#10;&lt;h3 id="fazit-1"&gt;Fazit&lt;/h3&gt;&#10;&lt;p&gt;Ich bin mit &lt;em&gt;MakerTube&lt;/em&gt; bis jetzt zufrieden: Transkodierungen gehen flott, Videos laufen flüssig. Sogar der &lt;em&gt;Heise-Verlag&lt;/em&gt; hat nun ein erstes Video auf die Instanz hochgeladen. Es ist allerdings deutlich schwieriger, ohne &amp;ldquo;Fanbase&amp;rdquo; auf die Landing Page zu kommen, was bei &lt;em&gt;scitech.video&lt;/em&gt; für mich nie ein Problem darstellte 😅.&lt;/p&gt;&#10;&lt;p&gt;Lasst mich mit einem weiteren Zitat von der Site selbst schließen:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;MakerTube is here to stay.&amp;rdquo; - &lt;em&gt;MakerTube&lt;/em&gt;, March 2025, about &lt;em&gt;MakerTube&lt;/em&gt;.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Danke, Jan. Auf die Zukunft!&lt;/p&gt;&#10;</description></item><item><title>MakerTube</title><link>https://blog.schallbert.de/announcements/2025-03-17-moving-to-makertube/</link><pubDate>Mon, 17 Mar 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/announcements/2025-03-17-moving-to-makertube/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/announcements/2025-03-17-makertube.jpg"&#10; class="post-cover"&#10; alt="Image: my channel on MakerTube called &amp;#39;CNC corner&amp;#39;."&#10; title="MakerTube" /&gt;&#10;&lt;p&gt;Ich bekam eine Mail mit dem Hinweis, dass die von mir genutzte PeerTube-Instanz &lt;code&gt;scitech.video&lt;/code&gt; Mitte April 2025 ihren Betrieb einstellen würde. Als Begründung nannte der Host unter anderem den hohen Zeit- und Kostenaufwand für den Betrieb sowie die geringe Anzahl Nutzer. Dazu gibt es inzwischen sogar einen eigenen Artikel, der sich mit den &lt;a href="https://blog.schallbert.de/fediverse-ups-and-downs/"&gt;Vor- und Nachteilen als Betreiber im Fediverse&lt;/a&gt; beschäftigt.&lt;/p&gt;&#10;&lt;p&gt;Als Nachfolgeportal für meine Videos habe ich &lt;a href="https://makertube.net/c/cnc_corner/videos" target="_blank" rel="noopener noreferrer" class="external-link"&gt;MakerTube&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ausgewählt. Dorthin migriere ich nun sämtliches Material. Stück für Stück in den kommenden Wochen.&lt;/p&gt;&#10;&lt;p&gt;MakerTube wird in Deutschland über Hetzner gehostet und hat viel, viel mehr Rechenpower als scitech. Daher laufen die Videos deutlich reibungsloser als vorher.&lt;/p&gt;&#10;&lt;p&gt;Abschließend werde ich alle Videolinks auf dieser Site umbiegen müssen, damit sie auch weiterhin funktionieren.&lt;/p&gt;&#10;&lt;p&gt;Für Nutzende von &lt;code&gt;schallbert.de&lt;/code&gt; ändert sich nichts 😀&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Schallbert&lt;/em&gt;&lt;/p&gt;&#10;</description></item><item><title>Gitea: Suchmaschinenindexierung stoppen</title><link>https://blog.schallbert.de/gitea-search-indexation/</link><pubDate>Wed, 12 Mar 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/gitea-search-indexation/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-03-12-gitea-indexation-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: How gitea recommends to remove search indexation"&#10; title="Gitea: Suchmaschinenindexierung stoppen" /&gt;&#10;&lt;p&gt;Nach ewigen Zeiten habe ich nur zum Spaß mal wieder einen Ausflug auf die Google Search Console gemacht. Normalerweise interessiert die mich überhaupt nicht, weil ich selbst Google gar nicht nutze und davon ausging, dass die Suchmaschinen ihre Arbeit aus Eigeninteresse schon gut machen würden.&lt;/p&gt;&#10;&lt;p&gt;Ich war etwas irritiert, als ich feststellte, dass es das Gros meiner Seiten nicht mal durch den Indexer geschafft hat.&lt;/p&gt;&#10;&lt;h2 id="das-problem"&gt;Das Problem&lt;/h2&gt;&#10;&lt;p&gt;Wie sollen denn Leute meine Webpräsenz finden? Selbst wenn sie die korrekten Suchbegriffe wüssten, würden sie über Google kaum etwas angezeigt bekommen. Immerhin stand klar drin, dass keiner meiner Blogartikel im Indexer vorkam. Auf meiner Gitea-Instanz hingegen waren mehrere tausend Seiten dem Indexer bekannt. Ein klares Missverhältnis. Wieso überhaupt erzeugt Gitea ein so hohes Dateiaufkommen?&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-03-12-searchconsole.avif" alt="Image: Google Search console view for git.schallbert.de. Thousands of pages are crawled that a real user will never be interested in."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Googles &lt;a href="https://en.wikipedia.org/wiki/Web_crawler" target="_blank" rel="noopener noreferrer" class="external-link"&gt;crawler&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (ein Programm, welches das Netz nach Inhalten durchsucht und für die Suche indexierbar macht) findet anscheinend jeden noch so kleinen Commit in öffentlichen Repositories sowie die dahinter liegenden Dateien. Auch Automationsläufe und andere Metadaten werden indexiert.&lt;/p&gt;&#10;&lt;p&gt;Das ist natürlich vollkommen unnötig und verbraucht mir an einer Stelle Energie, die ich viel lieber in der Präsentation meines Blogs hätte. Also muss eine Lösung her.&lt;/p&gt;&#10;&lt;h2 id="lösungsansätze"&gt;Lösungsansätze&lt;/h2&gt;&#10;&lt;p&gt;Mir fielen direkt ein paar Möglichkeiten ein:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Den Crawler für jedes Repository so konfigurieren, dass nur die oberste Ordnerebene durchsucht wird.&lt;/li&gt;&#10;&lt;li&gt;Suchindexierung für &lt;code&gt;git.schallbert.de&lt;/code&gt; komplett abschalten.&lt;/li&gt;&#10;&lt;li&gt;Per Search Console so lange Korrekturen fahren, bis der Indexer auf meinen Blogbeiträgen korrekte Zuordnungen anzeigt.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Die naheliegendste Lösung für mich war, den Indexer für meine Gitea-Instanz komplett abzuschalten. Korrekturschleifen greifen bei Googles Crawler erst Tage oder Wochen später und waren mir zu langwierig. Eine eigene &lt;code&gt;robots.txt&lt;/code&gt; für jedes Repository hielt ich für zu aufwendig.&lt;/p&gt;&#10;&lt;h2 id="meine-umsetzung"&gt;Meine Umsetzung&lt;/h2&gt;&#10;&lt;p&gt;Hierfür nahm ich die &lt;a href="https://docs.gitea.com/administration/search-engines-indexation" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea-Dokumentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Hand. Der Vorschlag mit einer &lt;code&gt;robots.txt&lt;/code&gt; leuchtete mir sofort ein, ebenso ihr Inhalt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;User-agent&lt;/span&gt;: *&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;Disallow&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hiermit wird Crawlern von jedem Anbieter &lt;code&gt;*&lt;/code&gt; gesagt, dass ab dem Stammverzeichnis &lt;code&gt;/&lt;/code&gt; überhaupt nicht indexiert werden soll.&lt;/p&gt;&#10;&lt;p&gt;Leider nur war mir überhaupt nicht klar, wie Gitea diese Datei in seiner Instanz bereitstellt:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;To make Gitea serve a custom robots.txt (default: empty 404) for top level installations, create a file with path &lt;code&gt;public/robots.txt&lt;/code&gt; in the &lt;a href="https://docs.gitea.com/administration/customizing-gitea" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;custom&lt;/code&gt; folder or &lt;code&gt;CustomPath&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="giteas-konfigurationsverzeichnis"&gt;Giteas Konfigurationsverzeichnis&lt;/h3&gt;&#10;&lt;p&gt;Also probierte ich ein Wenig herum. Erstellte mal einen &lt;code&gt;public&lt;/code&gt;-Ordner mit entsprechender Datei, dann einen &lt;code&gt;custom&lt;/code&gt;-Ordner auf verschiedenen Ebenen innerhalb der Gitea-Ordnerstruktur. Jedes mal startete ich den Container neu und schaute nach, ob die robots-Datei auch auf dem Server erschien.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-03-12-whererobots.avif" alt="Image: Where I put robots.txt in Gitea&amp;#39;s folder tree"&gt;&lt;/figure&gt;&#10;Nichts.&lt;/p&gt;&#10;&lt;p&gt;Doch dann löste ich mich von den Ordnernamen und packte die Datei schlicht in das Konfigurationsverzeichnis von Gitea - die niedrigste Ebene, auf der &lt;em&gt;gitea&lt;/em&gt; als Ordnername schon gar nicht mehr vorkommt. Und dies war die Lösung.&lt;/p&gt;&#10;&lt;h3 id="test"&gt;Test&lt;/h3&gt;&#10;&lt;p&gt;Nach Neustart konnte ich die Datei im Browser erfolgreich anzeigen.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-03-12-robotsolution.avif" alt="Image: Browser successfully loaded my robots.txt for the Gitea instance"&gt;&lt;/figure&gt;&#10;Inzwischen beginnt auch Google, die unerwünschten Seiten aus dem Indexer zu entfernen. Meine Blogartikel werden über die nächsten Wochen hoffentlich auch in der Google-Suche verfügbar.&lt;/p&gt;&#10;</description></item><item><title>Repo-Lookout: Sicherheitslücke beheben</title><link>https://blog.schallbert.de/repo-lookout-fix-deploy/</link><pubDate>Fri, 20 Dec 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/repo-lookout-fix-deploy/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-20-repolookout-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Repolookout reporting a possible vulnerability on my site"&#10; title="Repo-Lookout: Sicherheitslücke beheben" /&gt;&#10;&lt;h2 id="repo-informationen-öffentlich-zugänglich"&gt;Repo-Informationen öffentlich zugänglich&lt;/h2&gt;&#10;&lt;p&gt;Eines schönen Tages bekam ich eine E-Mail von &lt;a href="https://www.repo-lookout.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Repo Lookout&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Darin stand, dass eines meiner Repositories für Zugriffe aus dem Internet freigegeben sei. Dies stelle ein potentielles Sicherheitsrisiko dar, da möglicherweise geheime Quelldateien, versteckte Funktionen oder sogar Passwörter enthalten sein könnten.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-20_repolookout_warning.jpg" alt="Image: RepoLookout mail notification"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Zuerst hielt ich diese &amp;ldquo;Ihr-Repo-ist-nicht-sicher&amp;rdquo;-Warnung für einen Phishing-Versuch.&#10;Doch durch einfaches Eingeben der darin enthaltenen Links wurde klar, dass &lt;em&gt;Repo Lookout&lt;/em&gt; Recht hatte und mein &lt;a href="https://lectures.schallbert.de" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Lectures&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;-Repo nicht nur wie von mir gewünscht öffentlich war, sondern dass auch die Metadaten zur Versionskontrolle offen auf dem Webserver lagen.&lt;/p&gt;&#10;&lt;h3 id="ist-das-schlimm"&gt;Ist das schlimm?&lt;/h3&gt;&#10;&lt;p&gt;Normalerweise sollen innere Struktur und Konfiguration (Actions, Diskussionen, Wiki etc.) hinter einem Repository nicht öffentlich bleiben. Erst recht nicht, wenn das Repository als &lt;code&gt;private&lt;/code&gt; angelegt ist. Doch auch bei öffentlichen Repos sollte niemand auf die Struktur dahinter zugreifen können.&lt;/p&gt;&#10;&lt;p&gt;Daher die Mission von Repo-Lookout:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Find source code repositories that have been inadvertently exposed to the public and report them to the domain&amp;rsquo;s technical contact.&amp;rdquo; - Repo Lookout /about (&lt;a href="https://www.crissyfield.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Crissy Field GmbH&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;)&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;In diesem Falle unkritisch, aber unerwartet und unschön.&lt;/p&gt;&#10;&lt;p&gt;Schadet mir nicht, weil mein Webserver die Dateien lediglich zum Abruf dort liegen hat und sie selbst bei Manipulation keine Rückwirkung auf mein Repository gehabt hätten. Außerdem hatte ich alle Secrets wie &lt;a href="https://blog.schallbert.de/server-config-version-control/#secrets-in-docker-composeyml"&gt;in diesem Artikel beschrieben&lt;/a&gt; in eigens angelegte Dateien ausgelagert, sodass sie nicht mehr in den Konfigurationsdateien auftauchen. Das Repository liegt zudem getrennt auf der &lt;a href="https://git.schallbert.de/schallbert/lectures" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea-Server-Instanz&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Dennoch sollte nur am Internet hängen, was ich bewusst freigeben möchte.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-20_repo_commithistory.jpg" alt="Image: The repository&amp;#39;s commit history is public anyways"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Der Bildausschnitt von &lt;em&gt;Gitea&lt;/em&gt; zeigt denselben Commit wie die Warnung von &lt;em&gt;Repo Lookout&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h3 id="wie-es-dazu-kam"&gt;Wie es dazu kam&lt;/h3&gt;&#10;&lt;p&gt;In der Deploy-Pipeline für meine Subdomain &lt;code&gt;lectures.schallbert.de&lt;/code&gt; und Landing Page &lt;code&gt;schallbert.de&lt;/code&gt; habe ich einen direkt-Checkout von der &lt;em&gt;Gitea&lt;/em&gt;-Instanz zum Webserver &lt;em&gt;Caddy&lt;/em&gt;. Dieser läuft automatisch an, sobald die &lt;code&gt;main&lt;/code&gt;-Branches ein Update erhalten. Der Runner startet eine &lt;a href="https://github.com/marketplace/actions/checkout" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Checkout-Action&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, welche das Repository ins entsprechende Verzeichnis des Webservers kopiert.&lt;/p&gt;&#10;&lt;h3 id="checkout-action-kopiert-auch-den-git-ordner"&gt;Checkout-Action kopiert auch den &lt;code&gt;.git&lt;/code&gt;-Ordner&lt;/h3&gt;&#10;&lt;p&gt;Dabei wird der &lt;code&gt;.git&lt;/code&gt;-Ordner einfach mit aufgesetzt. Dort drin liegen alle von der Versionskontrollsoftware &lt;a href="https://git-scm.com/docs/gitrepository-layout" target="_blank" rel="noopener noreferrer" class="external-link"&gt;benötigten Daten zur Zustandsverwaltung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; des Repos. Den Kopiervorgang selbst kann ich auf &lt;em&gt;Gitea&lt;/em&gt; nicht sehen, denn dort taucht der versteckte &lt;code&gt;.git&lt;/code&gt;-Ordner gar nicht erst im Verzeichnis auf. Verständlich, denn auf Basis dieses Ordners findet die gesamte Darstellung auf &lt;em&gt;Gitea&lt;/em&gt; statt.&lt;/p&gt;&#10;&lt;p&gt;So blieb das unerwünschte Verhalten unter meinem Radar - und laut &lt;em&gt;Repo Lookout&lt;/em&gt; bin ich bei Weitem nicht der Einzige, dem es so geht.&lt;/p&gt;&#10;&lt;h2 id="option-1-fix-auf-dem-webserver"&gt;Option 1: Fix auf dem Webserver&lt;/h2&gt;&#10;&lt;p&gt;Die naheliegendste Lösung ist, den Zugriff auf die Datei serverseitig zu blockieren. Dies kostet nur wenige Ressourcen und ist leicht einzurichten.&lt;/p&gt;&#10;&lt;p&gt;Dieser Foreneintrag zeigt, wie das geht: &lt;a href="https://caddy.community/t/v2-hide-entire-folder-caddyfile/7234" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hide-entire-folder-caddyfile&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Für meine zu schützenden Dateien füge ich also folgende Einträge der &lt;code&gt;Caddyfile&lt;/code&gt; hinzu:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /caddy2/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;respond /.git/* &amp;#34;Access denied&amp;#34; 403&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;respond /.gitea/* &amp;#34;Access denied&amp;#34; 403&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Damit sage ich Caddy, dass es bei Aufruf einer beliebigen Datei &lt;code&gt;/*&lt;/code&gt; im Ordner &lt;code&gt;/.git&lt;/code&gt; mit dem Fehlercode &lt;code&gt;403&lt;/code&gt; &amp;ldquo;Forbidden&amp;rdquo; antworten soll. Dabei ist die Wildcard (&lt;code&gt;*&lt;/code&gt;) unbedingt erforderlich, denn sonst wird &lt;em&gt;lediglich der Ordner selbst&lt;/em&gt; und nicht die enthaltenen Dateien gesperrt.&lt;/p&gt;&#10;&lt;p&gt;Zur Kontrolle prüfe ich was passiert, wenn ich die Git Logs anfrage:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;# Terminal&#10;curl &amp;lt;lectures.schallbert.de&amp;gt;/.git/logs/HEAD&#10;Access denied &#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Funktioniert prima!&lt;/p&gt;&#10;&lt;h2 id="option-2-fix-in-der-checkout-action"&gt;Option 2: Fix in der Checkout-Action&lt;/h2&gt;&#10;&lt;p&gt;Es gibt allerdings eine noch viel elegantere Lösung: bereits vorher in der Pipeline dafür sorgen, dass der Ordner gar nicht erst auf dem Server auftaucht.&lt;/p&gt;&#10;&lt;h3 id="variante-1-mit-hilfe-von-sparse-checkout"&gt;Variante 1: Mit Hilfe von &lt;code&gt;sparse-checkout&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/33933702/git-checkout-except-one-folder" target="_blank" rel="noopener noreferrer" class="external-link"&gt;sparse-checkout&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ermöglicht die Auswahl von Ordnern und Dateien, die zum Checkout gehören sollen. Alle anderen Dateien im Repository bleiben unberührt und tauchen im Branch nicht auf. Dies spart vor allem bei großen Repositories Zeit und Speicherplatz. Macht aber natürlich nur Sinn wenn bereits vorher bekannt ist, dass nicht alle Dateien angefasst werden müssen.&lt;/p&gt;&#10;&lt;h3 id="negativliste-für-sparse-checkout"&gt;Negativliste für &lt;code&gt;sparse-checkout&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;In meinem Falle will ich die oben erwähnten Ordner gerade &lt;em&gt;nicht&lt;/em&gt; per Checkout auf den Server kopieren, den ganzen Rest aber schon. Wie ich das hinbekomme? Mit Hilfe der Negation im &lt;code&gt;no-cone&lt;/code&gt; mode.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;The user has explicitly said &amp;lsquo;I want these directories and not those directories.&amp;rsquo;&amp;rdquo; - Derrick Stolee, Microsoft, auf &lt;a href="https://github.com/git/git/commit/55dfcf9591b088ce60ec80eb5425dda18223cac0" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;In der &lt;a href="https://github.com/marketplace/actions/checkout" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung zu Checkout-Action&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; steht, dass &lt;code&gt;sparse-checkout&lt;/code&gt; auch für die vom Runner automatisierte Aktion unterstützt wird.&lt;/p&gt;&#10;&lt;p&gt;Nun programmiere ich mit Hilfe des &lt;a href="https://github.github.com/actions-cheat-sheet/actions-cheat-sheet.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github Actions Cheet Sheet&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /.gitea/workflows/deploy-lectures.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;CHECKOUT ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v4&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./tmp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;sparse-checkout&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; /*&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; !.git&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; !.gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;sparse-checkout-cone-mode&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Zur Erläuterung: Das Skript zum Veröffentlichen auf meinem Webserver verwendet die Aktion &lt;code&gt;checkout&lt;/code&gt;, Unterfunktion &lt;code&gt;sparse-checkout&lt;/code&gt; und bezieht sämtliche Dateien im Ordner im Stammverzeichnis &lt;code&gt;tmp&lt;/code&gt; und darunter ein bis auf &lt;code&gt;.git&lt;/code&gt; und &lt;code&gt;.gitea&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="was-ist-der-no-cone-mode"&gt;Was ist der &lt;code&gt;No-Cone Mode&lt;/code&gt;?&lt;/h3&gt;&#10;&lt;p&gt;Standardmäßig erwartet &lt;code&gt;sparse-checkout&lt;/code&gt; eine Liste von Ordnern, die für den Checkout erfasst werden sollen. Im &lt;code&gt;no-cone&lt;/code&gt; Modus wird stattdessen eine Liste von Patterns erwartet. Hier sind sämtliche Operatoren möglich, die auch in der &lt;code&gt;.gitignore&lt;/code&gt; zum Spezifizieren von Dateien, Ordnern, Auslassungen etc. verwendet werden können. Dies ermöglicht mir den Ausschluss bestimmter Ordner, hat aber einige &lt;a href="https://git-scm.com/docs/git-sparse-checkout#_internalsnon_cone_problems" target="_blank" rel="noopener noreferrer" class="external-link"&gt;gewichige Nachteile&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Durch viel höhere Komplexität der Pattern-Befehle, die damit einhergehende Fehleranfälligkeit sowie die deutlich rechenintensivere Auswertung bei größeren Repositories wird die Verwendung des &lt;code&gt;no-cone&lt;/code&gt; Mode nicht empfohlen und ist in der Dokumentation als &amp;ldquo;deprecated&amp;rdquo; (überholt) angegeben. Dennoch, probieren geht über Studieren!&lt;/p&gt;&#10;&lt;h3 id="test-mit-sparse-checkout"&gt;Test mit &lt;code&gt;sparse-checkout&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Nun lade ich die Action auf meine Gitea-Instanz hoch und lass meinen &lt;em&gt;runner&lt;/em&gt; einmal drüber laufen. Anschließend logge ich mich auf dem Webserver ein und schaue, ob der &lt;code&gt;.git&lt;/code&gt;-Ordner erstellt wurde oder nicht:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;lectures# ls -la&#10;[...]&#10;drwxr-xr-x 8 root root 4096 Dec 20 10:41 .git&#10;[...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Mist, der Ordner ist ja immer noch da. Ich schaue in den Logs der Action auf meiner Gitea-Instanz nach:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;[...]&#10;hint: &#9;git branch -m &amp;lt;name&amp;gt;&#10;Initialized empty Git repository in /workspace/schallbert/lectures/tmp/.git/&#10;[...]&#10;::group::Setting up sparse checkout&#10;[command]/usr/bin/git config core.sparseCheckout true&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;An &lt;code&gt;sparse-checkout&lt;/code&gt; liegt es also nicht. Dafür aber an der Art und Weise, wie &lt;em&gt;checkout&lt;/em&gt; funktioniert: Offensichtlich wird für ein ordentliches Aufsetzen des Repository auf meinem Webserver der &lt;code&gt;.git&lt;/code&gt;-Ordner zwingend benötigt. Also bleibt mir nur übrig, ihn nach dem Auschecken automatisch zu löschen.&lt;/p&gt;&#10;&lt;h3 id="variante-2-rm--rf"&gt;Variante 2: &lt;code&gt;rm -rf&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Und so probiere ich es mit Gewalt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /.gitea/workflows/deploy-lectures.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;CHECKOUT ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v4&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./tmp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;REMOVE TEMPORARY FILES ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rm -rfv ./tmp/.git ./tmp/.gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nun endlich taucht der &lt;code&gt;.git&lt;/code&gt;-Ordner auf meinem Webserver nicht mehr auf und mein &amp;ldquo;Repo-Leak&amp;rdquo; ist geflickt. Nochmal danke an &lt;em&gt;Repo Lookout&lt;/em&gt;!&lt;/p&gt;&#10;&lt;h2 id="fazit"&gt;Fazit&lt;/h2&gt;&#10;&lt;p&gt;Ich hatte hier das Problem, dass der versteckte &lt;code&gt;.git&lt;/code&gt;-Ordner, wo Konfiguration und Struktur von Repositories gespeichert werden, unbeabsichtigt und ohne mein Wissen auf meinem Webserver veröffentlicht waren.&lt;/p&gt;&#10;&lt;p&gt;Ich habe hier zwei funktionierende Optionen zur Lösung des Problems vorgestellt:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Ein Zugriffsverbot auf dem Webserver einrichten&lt;/li&gt;&#10;&lt;li&gt;Die Pipeline so umbauen, dass sie den &lt;code&gt;.git&lt;/code&gt;-Ordner nach erfolgtem Ausrollen selbstständig löscht.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Die zweite Option ist zwar etwas aufwendiger einzubauen, doch packt sie das Problem bei der Wurzel, anstatt lediglich die Symptome zu beheben. Außerdem entspricht sie dem ersten Grundsatz aus dem Datenschutz: Datenminimierung geht vor Schutzmaßnahmen.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Was nicht existiert, kann nicht verloren gehen&amp;rdquo; - Schallbert&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;</description></item><item><title>Logrotate-Rekursion</title><link>https://blog.schallbert.de/logrotate-mistake/</link><pubDate>Mon, 18 Nov 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/logrotate-mistake/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-11-18_logrotate-recursion.jpg"&#10; class="post-cover"&#10; alt="Image: Logrotate creating .1 recursively"&#10; title="Logrotate-Rekursion" /&gt;&#10;&lt;h2 id="was-funktioniert-nicht"&gt;Was funktioniert nicht?&lt;/h2&gt;&#10;&lt;p&gt;Eigentlich wollte ich &lt;a href="https://github.com/logrotate/logrotate" target="_blank" rel="noopener noreferrer" class="external-link"&gt;logrotate&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; nutzen, um meine Logs täglich zu bereinigen und so Datensparsamkeit umzusetzen: So können IP-Adressen, die ich für&amp;rsquo;s &lt;a href="https://blog.schallbert.de/server-protection/"&gt;Banning&lt;/a&gt; brauche, einheitlich und automatisiert nach kurzer Zeit gelöscht werden. Die Server-Logs interessieren mich ansonsten überhaupt nicht und stehlen mir nur Speicherplatz.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-11-18_logrotate-recursion.jpg" alt="Image: The way logrotate does the rotation looks all too recursive."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Mit meiner aktuellen Konfiguration erstellt &lt;code&gt;logrotate&lt;/code&gt; zwar eine Logdatei am Tag, nennt sie aber &lt;code&gt;xyz.log.1, xyz.log.1.1, xyz.log.1.1.1&lt;/code&gt; anstatt der erwarteten &lt;code&gt;xyz.log.1, xyz.log.2, xyz.log.3&lt;/code&gt;. Somit funktioniert die Löschroutine nicht mehr und die Anzahl der Logs wächst stetig.&lt;/p&gt;&#10;&lt;h2 id="und-warum"&gt;Und warum?&lt;/h2&gt;&#10;&lt;p&gt;Ich vermute, dass meine &lt;em&gt;logrotate&lt;/em&gt;-Konfiguration fehlerhaft ist. Im Moment sieht sie wie folgt aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## LOGROTATE file named &amp;#39;blog&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;blog/* {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# it&amp;#39;s ok if the file doesn&amp;#39;t already exist&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;missingok&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Sets the logs to rotate in intervals&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;daily&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Tells the system to remove old logs and only keep the most recent rotated logs&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;rotate 7&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Rotated logs will be compressed and kept on disk if they are 10 MB or less.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;size 10M&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# compress and delaycompress: These two options are used together and indicate that &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# rotated logs should be compressed (gzip) except for the most recent one.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;compress&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;delaycompress&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wie üblich nehme ich als Erstes die Bedienungsanleitung zur Hilfe, die ich mittels &lt;code&gt;man logrotate&lt;/code&gt; in der Konsole aufrufe.&#10;Dort steht, dass die zu rotierende Datei direkt angegeben (und nicht nur per &amp;ldquo;Wildcard&amp;rdquo; gewählt &lt;code&gt;*&lt;/code&gt;) werden kann. Außerdem kann ein Logverhalten auch auf mehrere Dateipfade angewendet werden, indem sie durch Leerzeichen getrennt vor die geschweifte Klammer gesetzt werden.&lt;/p&gt;&#10;&lt;p&gt;Das Sternchen scheint genau mein Problem mit den kaskadierenden Logs zu erzeugen: Dadurch wird nicht nur die eigentliche Zieldatei &lt;code&gt;access.log&lt;/code&gt; rotiert sondern auch alle bereits rotierten Dateien nochmal angefasst.&lt;/p&gt;&#10;&lt;h2 id="die-lösung"&gt;Die Lösung&lt;/h2&gt;&#10;&lt;p&gt;Entsprechend der Bedienungsanleitung schreibe ich die Bearbeitungsvorschrift für Logrotate so um, dass ich statt fünf eigenen Vorschriften für die jeweiligen Logs nur zwei Dateien erzeuge. Diese sind zudem einfacher und kürzer als die obige Ausgangsversion.&lt;/p&gt;&#10;&lt;p&gt;Beispiel:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# rotate webserver log files.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;gitea/access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;blog/access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;landing/access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;lectures/access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Sets the logs to rotate in intervals&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;daily&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Tells the system to remove old logs and only keep the most recent rotated logs&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;rotate 7&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# compress and delaycompress: These two options are used together and indicate that &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# rotated logs should be compressed (gzip) except for the most recent one.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;compress&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;delaycompress&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="konfigurations-update"&gt;Konfigurations-Update&lt;/h3&gt;&#10;&lt;p&gt;Dies kann ich durch meine vorhandene Server-Konfigurations-Pipeline durch einfachen Push ins Repository auf dem Server ausrollen.&#10;Nun muss ich die Dateien dort nur noch händisch nach &lt;code&gt;/etc/logrotate.d&lt;/code&gt; kopieren.&lt;/p&gt;&#10;&lt;h3 id="logrotate-testen"&gt;Logrotate testen&lt;/h3&gt;&#10;&lt;p&gt;Jetzt wäre es noch schön, wenn ich ausprobieren könnte, ob die von mir vorgenommenen Änderungen auch tatsächlich funktionieren. Dafür nehme ich erneut die Bedienungsanleitung von &lt;em&gt;logrotate&lt;/em&gt; zur Hilfe und tippe:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;logrotate -d -v &amp;lt;LOGROTATE-DESCRIPTOR-FILE&amp;gt;&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Mit aktivierten &lt;em&gt;debug&lt;/em&gt; und &lt;em&gt;verbose&lt;/em&gt; Flags wird logrotate die in der Konfigurationsdatei angegebene Rotationsvorschrift anwenden und Rückmeldung geben, tatsächlich auf dem Dateisystem aber keine Änderungen oder Rotationen durchführen. Bei mir sieht das Ergebnis des Tests dann etwa so aus:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;lectures/access.log &#10; after 1 days (7 rotations)&#10;empty log files are rotated, old logs are removed&#10;considering log gitea/access.log&#10; Now: 2024-11-28 17:19&#10; Last rotated at 2024-11-27 00:00&#10; log needs rotating&#10;considering log blog/access.log&#10; Now: 2024-11-28 17:19&#10; Last rotated at 2024-11-16 00:00&#10; log needs rotating&#10;considering log landing/access.log&#10; Now: 2024-11-28 17:19&#10; Last rotated at 2024-11-07 00:00&#10; log needs rotating&#10;considering log lectures/access.log&#10; Now: 2024-11-28 17:19&#10; Last rotated at 2024-09-21 00:00&#10; log needs rotating&#10;rotating log gitea/access.log, log-&amp;gt;rotateCount is 7&#10;[...]&#10;renaming lectures/access.log.7.gz to lectures/access.log.8.gz (rotatecount 7, logstart 1, i 7), &#10;renaming lectures/access.log.6.gz to lectures/access.log.7.gz (rotatecount 7, logstart 1, i 6), &#10;renaming lectures/access.log.5.gz to lectures/access.log.6.gz (rotatecount 7, logstart 1, i 5), &#10;renaming lectures/access.log.4.gz to lectures/access.log.5.gz (rotatecount 7, logstart 1, i 4), &#10;renaming lectures/access.log.3.gz to lectures/access.log.4.gz (rotatecount 7, logstart 1, i 3), &#10;renaming lectures/access.log.2.gz to lectures/access.log.3.gz (rotatecount 7, logstart 1, i 2), &#10;renaming lectures/access.log.1.gz to lectures/access.log.2.gz (rotatecount 7, logstart 1, i 1), &#10;renaming lectures/access.log.0.gz to lectures/access.log.1.gz (rotatecount 7, logstart 1, i 0)&#10;[...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Die Ausgabe zeigt mir, dass sowohl alle gewünschten Logdateien von &lt;em&gt;logrotate&lt;/em&gt; berücksichtigt werden und auch, dass die Rotation korrekt und täglich durchgeführt wird.&lt;/p&gt;&#10;&lt;p&gt;Wieder etwas gelernt 😃&lt;/p&gt;&#10;</description></item><item><title>Neue Startseite</title><link>https://blog.schallbert.de/announcements/2024-10-01-new-landing-page/</link><pubDate>Tue, 01 Oct 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/announcements/2024-10-01-new-landing-page/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/announcements/2024-10-01-schallbert_landing-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumb for schallbert.de domain update"&#10; title="Neue Startseite" /&gt;&#10;&lt;h2 id="oktober-2024--neues-website-design-"&gt;Oktober-2024 &amp;mdash; Neues Website-Design &amp;mdash;&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/announcements/2024-10-01-schallbert_landing.jpg" alt="Image: Newly designed schallbert.de landing page"&gt;&lt;/figure&gt;&#10;&lt;a href="https://schallbert.de" target="_blank" rel="noopener noreferrer" class="external-link"&gt;schallbert.de&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; hat jetzt endlich die &amp;ldquo;Baustellenphase&amp;rdquo; überwunden. Ab jetzt steht nicht nur dieser Blog zur Verfügung, sondern auch die zusätzlichen Kategorien &lt;a href="https://blog.schallbert.de/announcements/2024-03-01-seifenbutler-out-now/"&gt;Shop&lt;/a&gt; und &lt;a href="https://lectures.schallbert.de" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Vorträge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="shop"&gt;Shop&lt;/h3&gt;&#10;&lt;p&gt;Dieser Bereich befindet sich noch in einer frühen Planungsphase. Ich überlege hier, meinen &lt;a href="https://blog.schallbert.de/projects/seifenbutler/"&gt;Seifenbutler&lt;/a&gt;, Rohlinge für CNC-Gravuren und ein paar meiner Software-Projekte wie zum Beispiel &lt;a href="https://blog.schallbert.de/projects/qr-codengrave/"&gt;QR-Codengrave&lt;/a&gt; als App zum Kauf anzubieten.&lt;/p&gt;&#10;&lt;p&gt;Ich habe mich bis jetzt weder auf ein Webshop-Framework festgelegt, noch bin ich momentan in der Lage, viel Zeit in den Aufbau der Seite oder die Fertigung von Produkten zu stecken. Von daher stellt der Shop ein Langfristiges Ziel dar. Als Bezugsquelle für Seifenbutler steht weiterhin &lt;a href="https://mopemaster.com/produkt/seifenbutler-seifenhalter-dusche/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Mopemasters Onlineshop&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Verfügung.&lt;/p&gt;&#10;&lt;h3 id="vorträge"&gt;Vorträge&lt;/h3&gt;&#10;&lt;p&gt;Diese Subdomain habe ich erstellt, um meine Vorträge zu sammeln. Sie nutzen ein anderes Framework als mein Blog und sind daher besser separat untergebracht.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Schallbert&lt;/em&gt;&lt;/p&gt;&#10;</description></item><item><title>NVME-Festplatte kaputt?</title><link>https://blog.schallbert.de/broken-nvme/</link><pubDate>Tue, 10 Sep 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/broken-nvme/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_nvme-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: My NVME-SSD that suddenly broke down"&#10; title="NVME-Festplatte kaputt?" /&gt;&#10;&lt;h2 id="das-ereignis"&gt;Das Ereignis&lt;/h2&gt;&#10;&lt;p&gt;Mitte April 2024:&lt;/p&gt;&#10;&lt;p&gt;Nach einem Systemupdate fährt mein Laptop plötzlich nicht mehr hoch. Der Bootloader &lt;em&gt;grub-core&lt;/em&gt; meldet, er habe Probleme beim Schreiben von Sektoren.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;error: ../grub-core/efi/efidisk.c:638: failure writing sector 0xXXXXXXXX&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Beim Reboot bleibt mir der Bildschirm nach Anzeige des Firmenlogos mit blinkendem Cursor schwarz. Wenn ich hinter die Maske schaue, dann schlägt das Laden diverser Services fehl:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-09-10_nvme-boot-errors.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Boot errors due to read-only nvme&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="erste-analyse"&gt;Erste Analyse&lt;/h2&gt;&#10;&lt;p&gt;Ich vermutete ein Problem beim Update des Rechners und führte aus dem &lt;a href="https://en.wikipedia.org/wiki/UEFI" target="_blank" rel="noopener noreferrer" class="external-link"&gt;UEFI&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; von Lenovo heraus einen Festplattentest durch. Ergebnis:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;The media check is complete, the result is: FAIL&#10;&lt;/code&gt;&lt;/pre&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_diskcheck-fail.jpg" alt="Image: disk check tool diagnoses a defective device"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Eher kein Problem mit dem Betriebssystem sondern eines mit der Platte. Mist. Muss ich um meine Daten bangen? Ich habe zwar Backups herumliegen, das letzte ist aber drei Wochen alt und somit sind einige Stunden Arbeit auf der Kippe. Zudem kostet mich ein Wiederaufsetzen einiges an Zeit. Daher probiere ich erst einmal, ob sich noch etwas retten lässt.&lt;/p&gt;&#10;&lt;h3 id="selbsttest"&gt;Selbsttest&lt;/h3&gt;&#10;&lt;p&gt;Ich lasse also die &lt;a href="https://en.wikipedia.org/wiki/Self-Monitoring,_Analysis_and_Reporting_Technology" target="_blank" rel="noopener noreferrer" class="external-link"&gt;SMART&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Tests für die Platte mit &lt;code&gt;smartctl -x /dev/nvm0&lt;/code&gt; durchlaufen und weiß nun, dass sie etwa &lt;code&gt;7600&lt;/code&gt; Betriebsstunden hinter sich und &lt;code&gt;240GB&lt;/code&gt; nutzbare Kapazität hat. Die unter dem Namen &lt;code&gt;nvme0n1&lt;/code&gt; eingesetzte Disk meint, sie sei nun &amp;ldquo;read-only&amp;rdquo;. Und zwar sämtliche Partitionen inklusive Kernel. Na klasse. Das Ergebnis meiner Online-Suche war in etwa:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Die Platte ist irreparabel beschädigt. Es muss eine Neue her.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="reparaturversuch"&gt;Reparaturversuch&lt;/h3&gt;&#10;&lt;p&gt;Man muss ja nicht alles glauben, was im Netz steht. Also nehme ich das Lenovo-eigene Reparaturwerkzeug für Festplatten zur Hand. Es lässt sich aus dem UEFI heraus aufrufen und firmiert unter dem Namen &lt;a href="https://download.lenovo.com/pccbbs/thinkvantage_en/userguide_uefi_v04.08.000.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Bad Block Recovery Tool&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Das Programm scannt die Platte auf defekte Sektoren und versucht, betroffene Blöcke wiederherzustellen. Der Durchlauf benötigt für die letzten 10% fast eine halbe Stunde.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_badblockrecovery_summary.jpg" alt="Image: Bad Block Recovery tool&amp;#39;s summary for my disk"&gt;&lt;/figure&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_badblockrecovery_detail.jpg" alt="Image: Bad Block Recovery tool&amp;#39;s output messages"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Ergebnis: Wiederherstellung fehlgeschlagen. Ich führe das Werkzeug erneut aus. Und noch ein drittes Mal im langsamen &lt;code&gt;pedantic&lt;/code&gt;-Modus. Immer wieder ohne Erfolg. Ständig bekomme ich &lt;code&gt;critical medium error&lt;/code&gt; angezeigt.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_badblockrecovery_log.jpg" alt="Image: Bad Block Recovery tool&amp;#39;s log"&gt;&lt;/figure&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Ob dies alles nicht klappt, weil sich die Platte in den Read-only mode gesetzt hat?&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Da ich nicht mehr booten kann und keine Konsole zur Verfügung habe, sind meine Möglichkeiten mit Bordmitteln nun erschöpft.&lt;/p&gt;&#10;&lt;h3 id="einwurf-der-read-only-mode-bei-ssds"&gt;Einwurf: Der Read-only mode bei SSDs&lt;/h3&gt;&#10;&lt;p&gt;Laut Internet &lt;a href="https://www.disktuna.com/a-write-protected-ssd-nvme-read-only/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;(DiskTuna)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist der &lt;em&gt;read only mode&lt;/em&gt; zur Schadensbegrenzung vorhanden. Stellt der Controller des Speichergerätes einen schwerwiegenden Fehler fest, versetzt er die Platte in Nur-Lesezugriff. Somit wird das Risiko minimiert, dass weitere Blöcke korrumpiert oder mit in den Tod gerissen werden.&lt;/p&gt;&#10;&lt;p&gt;Im Forum &lt;a href="https://superuser.com/questions/1802470/ssd-in-read-only-mode-after-power-failure" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Superuser&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; beschreibt jemand einen identischen Fehler zu meinem, und tatsächlich ist da wohl nichts mehr zu machen.&lt;/p&gt;&#10;&lt;h2 id="datenrettung-per-boot-stick"&gt;Datenrettung per Boot-Stick&lt;/h2&gt;&#10;&lt;p&gt;Weiter geht&amp;rsquo;s mit einem USB-Stick, den ich von einem anderen Rechner aus zum Linux-Boot-Stick mache. Ich stopfe ihn an meinen Laptop und kann ihn endlich wieder hochfahren. Als Medium zur Datenrettung schließe ich eine externe Festplatte an.&lt;/p&gt;&#10;&lt;h3 id="übersicht-verschaffen-mit-fdisk"&gt;Übersicht verschaffen mit &lt;em&gt;fdisk&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Zuerst schaue ich, ob alle Platten und Partitionen auftauchen. Dafür tippe ich das Kommando &lt;code&gt;sudo fdisk -l&lt;/code&gt; in die Konsole.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_fdisk.jpg" alt="Image: fdisk&amp;#39;s output on my live system"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Glück gehabt, alles ist noch da! Vielleicht kann ich die Dateien ja einfach von der defekten Festplatte herunter kopieren?&lt;/p&gt;&#10;&lt;h3 id="kopieren-mit-cp"&gt;Kopieren mit &lt;em&gt;cp&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Probieren wir&amp;rsquo;s aus.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo cp -r -v /mnt/nvme0n1p3/Documents/my/important/files /mnt/externalHdd/save&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ein paar Sekunden denke ich, dass alles glatt geht. Doch dann bricht &lt;code&gt;cp&lt;/code&gt; mit Fehlermeldung ab, das Volume sei nicht verfügbar. Tatsächlich ist die Platte nicht mehr gemountet. Ich kann sie allerdings direkt wieder finden und mounten. Ein paar Versuche später habe ich meine wirklich wichtigen Daten gesichert, der Kopiervorgang bricht aber stets nach ein paar Sekunden ab. Für die ganze Platte würde mir dieses Vorgehen viel zu lange dauern.&lt;/p&gt;&#10;&lt;p&gt;Genau dieses Verhalten - mounten, ein paar Sekunden kopieren, Platte verschwindet - habe ich online in ein paar Foreneinträgen bestätigt bekommen.&lt;/p&gt;&#10;&lt;p&gt;Also muss eine andere Lösung her.&lt;/p&gt;&#10;&lt;h3 id="daten-retten-mit-ddrescue"&gt;Daten retten mit &lt;em&gt;ddrescue&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_ddrescue.jpg" alt="Image: ddrescue output when saving my broken SSD&amp;#39;s files to an external HDD."&gt;&lt;/figure&gt;&#10;Nach kurzer Online-Suche installiere ich das Programm &lt;a href="https://www.gnu.org/software/ddrescue/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ddrescue&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;In der zugehörigen Dokumentation stehen einige Dinge sehr klar drin. Dass man z.B. verstanden haben sollte was es genau tut, bevor man es verwendet. Oder das Folgende:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Never try to repair a file system on a drive with I/O errors; you will probably lose even more data. &amp;quot; - &lt;a href="https://www.gnu.org/software/ddrescue/manual/ddrescue_manual.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ddrescue Manual&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Jetzt stellt sich für mich die Frage, ob ich oben mit dem Block-Reparaturversuch genau so einen Fehler begangen habe. Oder bin ich mit der Bad Block Recovery auf der sicheren Seite, da hier von Datenblöcken auf der Platte und nicht vom &amp;ldquo;darüber liegenden&amp;rdquo; Dateisystem die Rede ist?&#10;Hier steht auch, dass die sogenannte &lt;em&gt;Mapfile&lt;/em&gt; eine Kernfunktion des Programmes ist. Nach Abbruch, Neustart, für die Zusammenführung von Backups und zum Verkürzen der Datenrettung bietet sie einen großen Zeitvorteil. Daher sei es keine schlechte Idee, sich die Mapfile schon bei Erstellung einer Offlinhe-Sicherheitskopie zu erzeugen und gut wegzulegen. Insgesamt fand ich das Tool extrem hilfreich, effektiv und leicht zu bedienen und kann es an dieser Stelle nur weiterempfehlen.&lt;/p&gt;&#10;&lt;p&gt;Zurück zur Sache: Ich finde einen ganz einfachen Befehl, die komplette Datenpartition meines beschädigten NVME-Speichers auf eine externe Festplatte zu kopieren:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ddrescue --sparse /dev/nvme0n1p3 /run/media/liveuser/myExternalHDD/datapartition&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die &lt;code&gt;--sparse&lt;/code&gt;-Option überspringt mit Nullen belegte Bereiche der Quellplatte und kann so eine Menge Platz auf dem Zieldatenträger einsparen.&#10;Das Programm benötigt eine Viertelstunde für den kompletten Durchlauf. Es kann wie oben im Bild zu erkennen tatsächlich &lt;code&gt;99.6%&lt;/code&gt; der Daten retten und auch mehrere Monate später habe ich die restlichen &lt;code&gt;0.4%&lt;/code&gt; noch nicht vermisst. Glück gehabt.&lt;/p&gt;&#10;&lt;h3 id="überprüfung-output-von-ddrescue-ansehen"&gt;Überprüfung: Output von &lt;em&gt;ddrescue&lt;/em&gt; ansehen&lt;/h3&gt;&#10;&lt;p&gt;Jedenfalls legt das Programm am Ende eine einzige Datei auf meiner externen Platte ab: &lt;code&gt;datapartition.img&lt;/code&gt;&#10;Dieses Image kann ich mit &lt;code&gt;fdisk -l&lt;/code&gt; anschauen:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;datapartition.img: 236.89 GiB, 254356226048 bytes, 496789504 sectors&#10;Units: sectors of 1 * 512 = 512 bytes&#10;Sector size (logical/physical): 512 bytes / 512 bytes&#10;I/O size (minimum/optimal): 512 bytes / 512 bytes&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Alles da, das ist ja schon einmal gut. Ob denn nun auch alles drin ist?&lt;/p&gt;&#10;&lt;p&gt;Dafür mounte ich die Partition mal und schaue mich per &lt;code&gt;cd&lt;/code&gt; und &lt;code&gt;ls -l&lt;/code&gt; darin um:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mkdir /mnt/olddisk &lt;span style="color:#75715e"&gt;# Mount point of the rescued data image&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mount /run/media/liveuser/myExternalHDD/datapartition.img /mnt/olddisk&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Zum Glück war alles drin, was drin sein sollte. Nun konnte ich das Image unmounten, den Rechner herunterfahren und eine neue Festplatte einbauen lassen.&lt;/p&gt;&#10;&lt;h2 id="die-neue-festplatte"&gt;Die neue Festplatte&lt;/h2&gt;&#10;&lt;p&gt;Auch hier habe ich noch einmal Glück gehabt: Auf meinem Gebrauchtlaptop war noch Garantie und so bekam ich ihn binnen einer Woche mit einer neuen (und doppelt so großen) Festplatte zurück gesendet. Da ich so große Angst vor Datenklau bekam, hatte ich meine alte Platte vor dem Versenden ausgebaut. Dabei kam ich mir ganz schön paranoid vor.&lt;/p&gt;&#10;&lt;p&gt;Diese Arbeit hätte ich mir aber vielleicht sparen können, denn &lt;em&gt;ddrescue&lt;/em&gt; hat auch für paranoide Leute wie mich eine Software-Lösung parat: Die Option &lt;code&gt;--fill-mode&lt;/code&gt;.&#10;Hiermit können nach erfolgter Rettung die &amp;ldquo;noch guten&amp;rdquo; Blöcke und Sektoren gelöscht werden. Nur die ohnehin defekten Sektoren bleiben erhalten. Aus der &lt;a href="https://www.gnu.org/software/ddrescue/manual/ddrescue_manual.html#Fill-mode" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Online-Dokumentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ddrescue --fill-mode&lt;span style="color:#f92672"&gt;=&lt;/span&gt;+ --force /dev/zero bad_drive mapfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ob dies allerdings auch einer Platte wie meiner funktioniert hätte, die der On-Board-Controller auf &amp;ldquo;read-only&amp;rdquo; gesetzt hat, ist mir nicht klar. Egal.&lt;/p&gt;&#10;&lt;p&gt;Ich kann jetzt jedenfalls mit einem voll funktionsfähigen Laptop weiter Blog-Einträge verfassen ☺️.&lt;/p&gt;&#10;</description></item><item><title>Giteas act_runner: Startprobleme</title><link>https://blog.schallbert.de/fix-gitea-runner/</link><pubDate>Fri, 30 Aug 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/fix-gitea-runner/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-08-30_badgateway_runner-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Bad Gateway error message from Gitea&amp;#39;s act_runner at startup"&#10; title="Giteas act_runner: Startprobleme" /&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Gitea schickt `act_runner` in Rente&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-09-15T00:00:00Z"&gt;&#10; 15. Sept. 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Dieser Artikel bezieht sich auf eine Actions-Implementierung von Gitea, den &lt;code&gt;act_runner&lt;/code&gt;. Jener ist abgeleitet von &lt;a href="https://github.com/nektos/act" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nectos/act&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Gitea verwendet inzwischen &lt;a href="https://blog.gitea.com/release-of-runner-1.0.0/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;einen eigenen Runner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Der alte Runner sollte ausgetauscht werden. Weitere Infos: Blogartikel &lt;a href="https://blog.schallbert.de/build-deploy-hugo-with-actions-docker-caddy/"&gt;Hugo-Seite mit Gitea Actions, docker, und caddy bereitstellen&lt;/a&gt;&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="das-problem"&gt;Das Problem&lt;/h2&gt;&#10;&lt;p&gt;Seitdem ich diese Seite in Eigenregie betreibe, habe ich nach Neustart des &lt;em&gt;Gitea&lt;/em&gt;-Containers das Problem, dass der aktivierte &lt;em&gt;act_runner&lt;/em&gt; manchmal nicht mit hochfährt. Er bricht dann unter Angabe von Status Code &lt;code&gt;-1&lt;/code&gt; ab und meine Workflows werden nicht abgearbeitet, sodass ich z.B. keine Artikel bauen oder veröffentlichen kann.&lt;/p&gt;&#10;&lt;p&gt;Die Fehlermeldung ist immer dieselbe:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;[...]&#10;runner-1 | time=&amp;#34;2024-08-26T10:09:46Z&amp;#34; level=info msg=&amp;#34;Starting runner daemon&amp;#34;&#10;runner-1 | time=&amp;#34;2024-08-26T10:09:46Z&amp;#34; level=error msg=&amp;#34;fail to invoke Declare&amp;#34; error=&amp;#34;unavailable: 502 Bad Gateway&amp;#34;&#10;runner-1 | Error: unavailable: 502 Bad Gateway&#10;runner-1 exited with code 1&#10;gitea | 2024/08/26 10:09:46 cmd/web.go:242:runWeb() [I] Starting Gitea on PID: 16&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Der Runner kann sich also nicht bei &lt;em&gt;Gitea&lt;/em&gt; einklinken. Wenn ich alle Services über Docker per &lt;code&gt;docker restart&lt;/code&gt; erneut starte, meint der Container für &lt;code&gt;act_runner&lt;/code&gt;, ihm fehle dafür ein Netzwerk mit ID &lt;code&gt;&amp;lt;ellenlanger Hexcode hier&amp;gt;&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h2 id="die-kurzfristige-lösung"&gt;Die kurzfristige Lösung&lt;/h2&gt;&#10;&lt;p&gt;Ich hatte die letzten Monate keine Zeit dem nachzugehen. Daher half ich mir stets damit, folgendes doppelt auszuführen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# schallbert server-console&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose -f /path/to/giteas/composefile up -d&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Beim zweiten Anlauf dann lief der Runner wiederholbar erfolgreich an (Gitea war ja schon betriebsbereit):&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;gitea | 2024/08/26 10:38:17 routers/init.go:116:InitWebInstalled() [I] Git version: 2.45.2 (home: /data/gitea/home)&#10;runner-1 | time=&amp;#34;2024-08-26T10:38:22Z&amp;#34; level=info msg=&amp;#34;Starting runner daemon&amp;#34;&#10;runner-1 | time=&amp;#34;2024-08-26T10:38:22Z&amp;#34; level=info msg=&amp;#34;runner: action-runner, with version: v0.2.10, with labels: [ubuntu-latest], declare successfully&amp;#34;&#10;runner-1 exited with code 0&#10;runner-1 | time=&amp;#34;2024-08-26T10:40:32Z&amp;#34; level=info msg=&amp;#34;Started runner daemon&amp;#34;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Dies ist natürlich nicht so schön und muss händisch von mir gemacht werden. Doch was ist die Ursache?&lt;/p&gt;&#10;&lt;h2 id="ursachenforschung"&gt;Ursachenforschung&lt;/h2&gt;&#10;&lt;p&gt;Zuerst hatte ich den Reverse Proxy von &lt;em&gt;Caddy&lt;/em&gt; im Verdacht, da er &lt;em&gt;Gitea&lt;/em&gt; an das Internet anbindet. Doch tatsächlich ist &lt;em&gt;Gitea&lt;/em&gt; selbst das Problem: Es ist zum Zeitpunkt, zu dem &lt;em&gt;Docker&lt;/em&gt; den &lt;em&gt;runner&lt;/em&gt; Container startet, noch gar nicht fertig hochgefahren. Also lese ich nach, wie ich Abhängigkeiten in Docker besser darstellen kann.&lt;/p&gt;&#10;&lt;h2 id="lösung-abhängigkeiten-abbilden"&gt;Lösung: Abhängigkeiten abbilden&lt;/h2&gt;&#10;&lt;p&gt;Zuerst versuche ich es mit dem Einbinden der Änderung in docker compose.&lt;/p&gt;&#10;&lt;h3 id="depends_on"&gt;depends_on&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;## service: runner&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;## [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;depends_on&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;gitea&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;condition&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;service_started&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Doch noch immer startet der &lt;em&gt;act_runner&lt;/em&gt; wohl zu früh. Docker stellt hier nämlich lediglich sicher, dass &lt;em&gt;Gitea&lt;/em&gt; gestartet wurde, nicht aber, dass es hochgefahren ist und stabil läuft.&lt;/p&gt;&#10;&lt;h3 id="healthcheck"&gt;Healthcheck&lt;/h3&gt;&#10;&lt;p&gt;Ich muss folglich sicherstellen, dass &lt;em&gt;Gitea&lt;/em&gt; betriebsbereit ist. Dies geht prima mit der Bedingung &lt;code&gt;service_healthy&lt;/code&gt;. Ich passe also die Instruktionen für den Runner wie folgt an:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;## service: runner&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;## [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;depends_on&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;gitea&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;condition&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;service_healthy&lt;/span&gt; &lt;span style="color:#75715e"&gt;# required so runner can attach to gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;restart&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Der &lt;code&gt;service_healthy&lt;/code&gt; Qualifier ist ein Rückgabewert der &lt;code&gt;healthcheck&lt;/code&gt; Funktion. Diese besteht aus einem sogenannten &lt;code&gt;test&lt;/code&gt; und ein paar Begleitparametern, in welchem Intervall wie oft und nach welcher Zeit geprüft werden soll, ob der Dienst nun betriebsbereit ist.&lt;/p&gt;&#10;&lt;p&gt;Ich habe als Test ein einfaches Kommando gewählt mit der Annahme, dass Gitea (und damit implizit auch Caddy) fertig hochgefahren ist, sobald &lt;code&gt;curl&lt;/code&gt; seine Webseite aufrufen kann.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## service: gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;## [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;healthcheck&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;test&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;CMD&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;curl&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;-f&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;https://git.schallbert.de/&amp;#34;&lt;/span&gt;] &lt;span style="color:#75715e"&gt;# checks if gitea is available&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;interval&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;10s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;retries&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;start_period&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;30s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;timeout&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;10s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mit diesen Zeilen prüft &lt;em&gt;Docker&lt;/em&gt;, ob Gitea &amp;ldquo;healthy&amp;rdquo; ist und startet den &lt;em&gt;act_runner&lt;/em&gt; entsprechend später. Das oben dargestellte Problem ist auf meinem Server seitdem komplett behoben und &lt;em&gt;Gitea&lt;/em&gt; sowie &lt;em&gt;runner&lt;/em&gt; verhalten sich zuverlässig und stabil.&lt;/p&gt;&#10;</description></item><item><title>Fehler-Logdateien versenden</title><link>https://blog.schallbert.de/server-deploy-logging/</link><pubDate>Tue, 20 Aug 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/server-deploy-logging/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-08-20_deploy_logging-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: A notification message sent on error"&#10; title="Fehler-Logdateien versenden" /&gt;&#10;&lt;p&gt;In diesem Artikel schaue ich mir an, wie ich &amp;ldquo;Monitoring&amp;rdquo; für meinen Server einrichte. Anwendungen und Dienste sollen mir im Fehlerfalle Benachrichtigungen senden können.&lt;/p&gt;&#10;&lt;h2 id="worum-geht-es-hier"&gt;Worum geht es hier?&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Sendemechaniken kennenlernen und auswählen&lt;/li&gt;&#10;&lt;li&gt;Testnachrichten verfassen und die Automation verifizieren&lt;/li&gt;&#10;&lt;li&gt;Fehlerbericht von &lt;em&gt;Borgmatic&lt;/em&gt; automatisch senden&lt;/li&gt;&#10;&lt;li&gt;Senden von Runner-logs durch &lt;em&gt;Gitea&lt;/em&gt;&lt;/li&gt;&#10;&lt;li&gt;Benachrichtigung bei Login auf meinem Server per &lt;em&gt;ssh&lt;/em&gt;&lt;/li&gt;&#10;&lt;li&gt;Erstellen und Senden eigener Logs zu Server-Update / Server-Fehlern&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="verifikation-und-überwachung"&gt;Verifikation und Überwachung&lt;/h3&gt;&#10;&lt;p&gt;Ich möchte nach dem Ausführen einer Automation wissen, ob sie erfolgreich durchgeführt wurde und alle Programme und Dienste ihre Arbeit erwartungsgemäß aufgenommen haben. Dies soll für jegliche Automation gelten - ob es sich konkret um das Senden eines Updates, die Anfertigung automatischer Backups oder eine Action von &lt;em&gt;Gitea&lt;/em&gt; handelt, muss egal sein.&lt;/p&gt;&#10;&lt;p&gt;Normalerweise würde ich für so etwas Berichtsmechanismen von &lt;em&gt;act_runner&lt;/em&gt; verwenden. Im Falle eines Server-Updates steht der Runner durch den bereits im Artikel &lt;a href="https://blog.schallbert.de/server-config-deploy/"&gt;Serverkonfiguration ausrollen&lt;/a&gt; erwähnten &lt;a href="https://blog.schallbert.de/server-config-version-control/#vor%c3%bcberlegungen"&gt;Zirkelbezug&lt;/a&gt; nicht zur Verfügung, da zeitweise alle Anwendungen heruntergefahren werden müssen.&lt;/p&gt;&#10;&lt;p&gt;Zudem kann es sein, dass Anwendungen eigene Vorgehensweisen haben, Monitoring zu betreiben. Also muss ich mir Mechanismen anschauen, mit denen ich leicht an die Informationen herankomme.&lt;/p&gt;&#10;&lt;h3 id="was-zeichnet-ein-gutes-monitoring-für-mich-aus"&gt;Was zeichnet ein gutes Monitoring für mich aus?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Es ist unaufdringlich, meldet sich also nur im Fehlerfall oder bei ungewöhnlichen Vorgängen.&lt;/li&gt;&#10;&lt;li&gt;Es liefert spezifische Informationen und gut zu verstehende Fehlermeldungen.&lt;/li&gt;&#10;&lt;li&gt;Es nutzt einen Nachrichtenkanal, der auch bei Absturz des zu überwachenden Systems funktioniert.&lt;/li&gt;&#10;&lt;li&gt;Es stellt Berichte und Fehlermeldungen isoliert von anderen Themen dar und vermischt nichts.&lt;/li&gt;&#10;&lt;li&gt;Es fasst sich kurz.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="methoden-automatischer-berichterstattung"&gt;Methoden automatischer Berichterstattung&lt;/h2&gt;&#10;&lt;p&gt;Diesen Abschnitt gliedere ich ich auf. Im Allgemeinen Teil gehe ich auf das mir auf dem Ubuntu-Server zur Verfügung stehende Bordwerkzeug zum asynchronen Monitoring ein. Danach schaue ich mir die bei meinen Diensten teils eingebauten Lösungen bzw. mit ihnen kompatible Anwendungen an. Dabei möchte ich mich nicht auf das klassische Instrument der E-Mail beschränken, sondern auch kurz &amp;ldquo;modernere&amp;rdquo; Kommunikationskanäle wie Messengerprogramme oder RSS-Feed beleuchten.&lt;/p&gt;&#10;&lt;h3 id="mail-per-konsole---curl"&gt;Mail per Konsole - &lt;em&gt;curl&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Versand von Mails als Benachrichtigungen sind in vielen Firmen üblich. Dies geht mit Linux meist sogar ganz ohne zusätzliche Programme: Das standardmäßig vorhandene &lt;a href="https://curl.se/docs/manpage.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;curl&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kann hier bereits weiterhelfen.&#10;&lt;em&gt;curl&lt;/em&gt; it ein Programm zum Datentransfer von oder zu einem Server. Gebe ich meinen Blog als Ziel ein, bekomme ich die HTML-Seite als Textdatei auf die Konsole ausgegeben:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl https://blog.schallbert.de&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bei der Gelegenheit fällt mir auf, wie viel unnötige Daten meine Blogsoftware erzeugt. Da werde ich später mal aufräumen müssen. Zurück zum Thema: Man kann &lt;em&gt;curl&lt;/em&gt; allerdings auch nutzen, um ein beliebiges Webbackend anzusprechen - Zum Beispiel einen Mailserver:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# copied from https://stackoverflow.com/questions/8260858/how-to-send-email-from-terminal&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl --url &lt;span style="color:#e6db74"&gt;&amp;#39;smtps://smtp.gmail.com:465&amp;#39;&lt;/span&gt; --ssl-reqd &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --mail-from &lt;span style="color:#e6db74"&gt;&amp;#39;from-email@gmail.com&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --mail-rcpt &lt;span style="color:#e6db74"&gt;&amp;#39;to-email@gmail.com&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --user &lt;span style="color:#e6db74"&gt;&amp;#39;from-email@gmail.com:YourPassword&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -T &amp;lt;&lt;span style="color:#f92672"&gt;(&lt;/span&gt;echo -e &lt;span style="color:#e6db74"&gt;&amp;#39;From: from-email@gmail.com\nTo: to-email@gmail.com\nSubject: Curl Test\n\nHello&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dies funktioniert nur dann auch für meinen Mail-Provider, wenn ich Logins durch externe Clients zulasse. Google nennt diese z.B. &amp;ldquo;weniger sichere Apps&amp;rdquo;. Ich bin wie in meinem Beitrag &lt;a href="https://blog.schallbert.de/server-config-version-control/#frisch-ans-werk"&gt;Server-Konfiguration mit Git&lt;/a&gt; beschrieben kein Fan davon, Secrets irgendwo hart hineinzuschreiben - daher würde ich den direkten Weg über &lt;em&gt;curl&lt;/em&gt; eher nicht nutzen.&lt;/p&gt;&#10;&lt;h3 id="mail-per-konsole---mail-mailx-mailutils-swaks"&gt;Mail per Konsole - &lt;em&gt;mail&lt;/em&gt;, &lt;em&gt;mailx&lt;/em&gt;, &lt;em&gt;mailutils&lt;/em&gt;, &lt;em&gt;swaks&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Wer all die Konfigurationsinformationen für den Server und Secrets nicht immer mitgeben möchte, dem stehen diverse handliche Werkzeuge für die Konsole wie &lt;a href="https://mailutils.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;mailutils&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; oder &lt;a href="https://github.com/jetmore/swaks" target="_blank" rel="noopener noreferrer" class="external-link"&gt;swaks&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Verfügung. Hier wird die Verbindung zum Mailserver über das Werkzeug einmalig konfiguriert und kann in z.B. in Umgebungsvariablen abgelegt werden. Die Syntax variiert von Programm zu Programm, hinten kommt aber stets eine Mail heraus.&lt;/p&gt;&#10;&lt;p&gt;An sich eine simple Lösung. Der einzige große Nachteil dabei für mich ist, dass sich hier Informationsdomänen vermischen. Ich hätte nur ungern noch einen &amp;ldquo;Berichtsthread&amp;rdquo; in meinen Mails, nach dem ich im Wust der Nachrichten irgendwann würde suchen müssen. Bei Benachrichtigungen auf anderen Kanälen kann ich zudem ein automatisches Verfallsdatum einstellen, sodass sie nach eingestellter Zeit aus meiner Liste verschwinden.&lt;/p&gt;&#10;&lt;h3 id="rss-feed-erzeugen"&gt;RSS Feed erzeugen&lt;/h3&gt;&#10;&lt;p&gt;Ungewöhnlich aber möglich: Ich könnte das Monitoring als &lt;a href="https://en.wikipedia.org/wiki/RSS" target="_blank" rel="noopener noreferrer" class="external-link"&gt;RSS-Feed&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; wie meinen Blog auch (&lt;a href="https://blog.schallbert.de/index.xml"&gt;schallberts-blog-feed&lt;/a&gt;) z.B. per Jekyll-Instanz erzeugen und als Website online stellen. Dieser wäre leicht zu abonnieren, mit praktisch jedem Reader zu lesen und ich könnte ihn sogar für jede Applikation ganz einfach getrennt vorsehen. Doch es gibt offensichtliche Nachteile:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Hoher Aufwand: Gitea runner mit Jekyll-Instanz, Webserver und Subdomain erforderlich.&lt;/li&gt;&#10;&lt;li&gt;Öffentlich verfügbar: Plötzlich werden Bauvorgänge, Updates, Upgrades und Fehlermeldungen für jedermann zugänglich.&lt;/li&gt;&#10;&lt;li&gt;Fehleranfällig: Schmiert Gitea, der Runner, mein Proxy oder der Webserver ab, bekomme ich keine Berichte.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Spätestens mit dem letzten Punkt ist diese Option für mich raus. Ich möchte ja gerade dann einen Bericht bekommen, wenn meine Applikationen nicht das tun, was sie sollen.&lt;/p&gt;&#10;&lt;p&gt;Werfen wir nun mal einen Blick auf die von mir bereits betriebenen Anwendungen und schauen, wie sie dieses Problem angehen.&lt;/p&gt;&#10;&lt;h3 id="borgmatic"&gt;&lt;em&gt;borgmatic&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Borgmatic bringt von Haus aus Kompabitilität zu einer Menge &lt;a href="https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Monitoring-Optionen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; mit. Darunter befinden sich &lt;a href="https://github.com/caronc/apprise" target="_blank" rel="noopener noreferrer" class="external-link"&gt;apprise&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://ntfy.sh/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ntfy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://healthchecks.io/docs/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;healthchecks&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://cronitor.io/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cronitor&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://www.pagerduty.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;pagerduty&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://cronhub.io/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cronhub&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und &lt;a href="https://grafana.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;grafana&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;em&gt;Apprise&lt;/em&gt; ist eine Bibliothek. Sie ist quelloffen und kann von einer bestehenden Anwendung als Abhängigkeit eingebunden werden. Wie ein Adapter ermöglicht sie die asynchrone Kommunikation der Anwendung mit diversen verschiedenen Kommunikationsdiensten wie SMS, Mail, Messenger (z.B. &lt;a href="https://signal.org/de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Signal&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;), diverse Heimautomation oder den Benachrichtigungssystemen diverser Betriebssysteme. Letzteres funktioniert allerdings nur auf der lokalen Maschine. Der Trigger für die Kommunikation muss dabei stets von der Anwendung ausgehen.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;ntfy&lt;/em&gt; ist ein Push-Benachrichtigungsdienst. Er ist quelloffen und kann sowohl selbst gehostet als auch über eine Webanwendung als Dienstleistung in Anspruch genommen werden. &lt;em&gt;Apprise&lt;/em&gt; z.B. unterstützt &lt;em&gt;ntfy&lt;/em&gt; als Kommunikationsdienst. Der Aufbau ist recht einfach und funktioniert wie bei &lt;a href="https://de.wikipedia.org/wiki/MQTT" target="_blank" rel="noopener noreferrer" class="external-link"&gt;MQTT&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; auch über einen Publication-Subscription bzw. Broker-Client Mechanismus, allerdings HTTP-basiert.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;Healthchecks&lt;/em&gt; ist ein Dienst. Er ist quelloffen und kann selbst gehostet werden. Der Dienst ist dazu da, regelmäßige Aktivitäten zu überwachen und kann als Totmannschalter fungieren: Erfolgt anders als erwartet keine Rückmeldung vom überwachten Programm, so kann er selbst eine Fehlermeldung absetzen. Diese kann wiederum an diverse Kommunikationsdienste gesendet werden.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;Cronitor&lt;/em&gt; ist eine Monitoringlösung und Webanwendung, die neben den von mir benötigten Benachrichtigungen eine Menge Analysetools, Leistungsmessungen und Metriken - zumeist gegen Geld - bereitstellt. Es gibt zwar einen kostenfreien &amp;ldquo;Hacker&amp;rdquo;-Account mit begrenztem Funktionsumfang, jedoch ist auch dieses Werkzeug mir entschieden zu groß und zu komplex.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;PagerDuty&lt;/em&gt; sieht sich als kommerzielle &amp;ldquo;Operations&amp;rdquo; Plattform, die sowohl &amp;ldquo;Incident Management&amp;rdquo;, Automation, &amp;ldquo;Business Operations&amp;rdquo;, &amp;ldquo;AIOps&amp;rdquo; etc. bereitstellt. Ist für mich direkt raus. Spätestens beim Wort &amp;ldquo;AIOps&amp;rdquo; 😅&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;cronhub&lt;/em&gt; sieht mir wie eine kommerzielle Webanwendung aus, die ähnlich wie &lt;em&gt;Healthchecks&lt;/em&gt; Cron-Jobs erstellen, überwachen und bei Fehlern melden kann. Sie ist für mich uninteressant, denn sie scheint weder open source zu sein noch könnte ich sie selbst hosten.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;Grafana&lt;/em&gt; ist eine Open-Source-Webanwendung, die entweder selbst gehostet oder als Cloud-Dienst verwendet werden kann. Obwohl viele größere Unternehmen und Projekte die Anwendung nutzen, ist sie für meine Zwecke um Größenordnungen zu umfang- und funktionsreich.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="fail2ban"&gt;&lt;em&gt;fail2ban&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Fail2ban hat keine Automationslösung wie &lt;em&gt;borgmatic&lt;/em&gt;. Es erzeugt schlicht Logdateien die ausgewertet werden müssten, um Inhalte für Benachrichtigungen zu gewinnen. Mir fällt im Moment nichts ein, was ich unbedingt von Fail2ban wissen müsste. Daher erstelle ich mir hierfür erst einmal keine Berichte.&lt;/p&gt;&#10;&lt;h3 id="gitea"&gt;&lt;em&gt;Gitea&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Bei Gitea lassen sich die Logdateien sehr feingliedrig konfigurieren. So können Access Logs getrennt von Service Logs, Repository Logs oder Action Logs herausgeschrieben und dann weiterverarbeitet werden. Als Benachrichtigungssystem bietet Gitea nach meiner Recherche ausschließlich einen &lt;a href="https://docs.gitea.com/next/administration/config-cheat-sheet#mailer-mailer" target="_blank" rel="noopener noreferrer" class="external-link"&gt;mailer&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; an. Dieser ist meiner Ansicht nach vor Allem für Repository- und Action Logs gedacht.&lt;/p&gt;&#10;&lt;p&gt;Auch hier würde es mir am meisten bringen, die Logs zu analysieren und bei Bedarf einen Bericht selbst zu erstellen.&lt;/p&gt;&#10;&lt;h3 id="server"&gt;Server&lt;/h3&gt;&#10;&lt;p&gt;Das Ausrollen meiner Server-Konfigurationsdateien übernehme ich per Skript ja sowieso schon selber. Also könnte ich auch hier im Fehlerfalle die Logs in eine Datei umleiten und sie dann einem Bericht in einen beliebigen Kanal anhängen.&lt;/p&gt;&#10;&lt;p&gt;Außerdem wären erfolgreiche Logins auf dem Server selbst eine Nachricht an mich wert. Dann kann ich sofort feststellen, ob ich das selbst war oder nicht.&lt;/p&gt;&#10;&lt;h2 id="die-auswahl-des-berichtsprogramms"&gt;Die Auswahl des Berichtsprogramms&lt;/h2&gt;&#10;&lt;p&gt;Ich muss nicht nur ein Monitoring-Programm heraussuchen, sondern auch einen Kommunikationsdienst wählen über den die Berichte versendet werden.&lt;/p&gt;&#10;&lt;h3 id="berichtsprogramm"&gt;Berichtsprogramm&lt;/h3&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Programm&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Vorteil&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Nachteil&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;em&gt;curl&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;simpel&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Konfiguration muss mitgegeben werden&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;funktioniert super einfach mit &lt;em&gt;ntfy&lt;/em&gt;&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Keine Überwachung bei Absturz des Servers&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Bordwerkzeug&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;kein Service erforderlich&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;em&gt;Apprise&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;ermöglicht Einbindung der Signal-Api&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Komplex im Zusammenspiel mit externen Diensten&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;kein Service erforderlich&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;em&gt;Healthchecks&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kann als Watchdog/Totmannschalter agieren&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Registrierung erforderlich&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kann lokal gehostet&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt; werden&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Lokales Hosting widerspricht Watchdog-Konzept&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Ich habe ja nur einen kleinen Server mit ein paar wenigen Anwendungen. Daher halte ich es hier so einfach wie möglich und wähle &lt;em&gt;Apprise&lt;/em&gt; für Dienste wie &lt;em&gt;borgmatic&lt;/em&gt;, die diese Bibliothek sowieso mitbringen und &lt;em&gt;curl&lt;/em&gt; für jene, deren Berichtsmechanik ich selbst programmieren muss.&lt;/p&gt;&#10;&lt;h3 id="ntfy"&gt;Kommunikationsweg&lt;/h3&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Programm&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Vorteil&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Nachteil&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;SMS&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Hohe Zuverlässigkeit&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Registrierung erforderlich&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Beschränkung auf wenige Zeichen&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Keine Anhänge möglich&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Keine Topics, Domänenvermischung&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Mail&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;einfach einzurichten&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Schlechte Durchsuchbarkeit&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;RSS-Feed&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Gute Sortierbarkeit&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kompliziert einzurichten, fehleranfällig&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Client sehr leichtgewichtig&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Lokales Hosting widerspricht Watchdog-Konzept&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Inhalte öffentlich verfügbar&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;em&gt;ntfy&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Simpel und leichtgewichtig&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Registrierung optional bei Web-Nutzung&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Client zweckorientiert&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Unsicher: Keine Verschlüsselung ohne Registrierung&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kostenlos für Kleinanwender&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Unabhängig von der eigenen Maschine&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;em&gt;Signal&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&amp;ldquo;Note to self&amp;rdquo; einfach einzurichten&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Einrichtung und Konfiguration der Signal-API komplex&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Unabhängig von der eigenen Maschine&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Topics etwas schwierig umzusetzen&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Nutzlos bei Absturz des Servers&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;SMS, Mail und RSS-Feed sind durch teils oben bereits erläuterte Nachteile für mich bereits raus. Bleiben also &lt;em&gt;ntfy&lt;/em&gt; und &lt;em&gt;Signal&lt;/em&gt;. &lt;em&gt;ntfy&lt;/em&gt; besticht durch seine Einfachheit: HTTP PUSH-request auf ein selbst definiertes &amp;ldquo;Topic&amp;rdquo; senden und selbiges auf dem Handy abonnieren - fertig. Es ist auch gut erweiterbar, denn durch self-hosting kann ich später bei Bedarf die Sicherheit erhöhen (Verschlüsselung) und das Sendeverhalten steuern. &lt;em&gt;Signal&lt;/em&gt; hingegen macht einen eigenen Client mit verhältnismäßig aufwendiger Konfiguration erforderlich. Außerdem müssten die Geräte Server-Handy verbunden werden, sodass nicht einfach neue Abonnenten hinzuzufügen sind. Dafür ist die Übertragung bestens Ende-zu-Ende gesichert, ich muss mich nicht registrieren und kostenlos ist es auch noch.&lt;/p&gt;&#10;&lt;p&gt;Vorerst entscheide ich mich auch hier für die weniger komplexe Lösung mit &lt;em&gt;ntfy&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h2 id="logging"&gt;Logging&lt;/h2&gt;&#10;&lt;p&gt;Gut. Nun ist geklärt, dass ich per &lt;em&gt;curl&lt;/em&gt; und &lt;em&gt;apprise&lt;/em&gt; über den &lt;em&gt;ntfy&lt;/em&gt;-Dienst nach Hause telefoniere. Nun schaue ich mal, welcher Inhalt zu übermitteln ist und wie ich möglichst unaufdringlich, dabei aber kurz und prägnant bleibe.&lt;/p&gt;&#10;&lt;h3 id="wann-sollen-logs-gesendet-werden"&gt;Wann sollen Logs gesendet werden?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;em&gt;borgmatic&lt;/em&gt;: Wenn ein Backup fehlschlägt&lt;/li&gt;&#10;&lt;li&gt;Bei erfolgreichem Login auf meinen Server oder Gitea&lt;/li&gt;&#10;&lt;li&gt;Nach Ausrollen eines Updates der Server-Konfiguration&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;docker&lt;/em&gt;: Falls eine Applikation ausfällt oder nicht hochfährt&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;gitea&lt;/em&gt;: Fehlgeschlagener Durchlauf von &lt;em&gt;act_runner&lt;/em&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="was-gehört-für-mich-in-einen-bericht"&gt;Was gehört für mich in einen Bericht?&lt;/h3&gt;&#10;&lt;p&gt;Ich möchte die klassischen &amp;ldquo;W-Fragen&amp;rdquo; beantwortet bekommen.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Wann ist es passiert (Zeitstempel)?&lt;/li&gt;&#10;&lt;li&gt;Welche Anwendung berichtet?&lt;/li&gt;&#10;&lt;li&gt;Was ist passiert?&lt;/li&gt;&#10;&lt;li&gt;Wo (Modul, Codezeile) etc. ist das passiert?&lt;/li&gt;&#10;&lt;li&gt;Wie viele Verletzte (Schweregrad, Recovery)?&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Eine Benachrichtigung sieht dann etwa so aus:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;TIMESTAMP APPLICATION PRIORITY MESSAGE EFFECT DETAIL &#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="rohfassung-eines-berichts"&gt;Rohfassung eines Berichts&lt;/h3&gt;&#10;&lt;p&gt;Eine &lt;em&gt;ntfy&lt;/em&gt;-Nachricht könnte ungefähr so aussehen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Title: Fehler Borgmatic&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Priority: urgent&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Tags: warning&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -d &lt;span style="color:#e6db74"&gt;&amp;#34;YYYY-MM-DD HH:MM:SS Backup-Erstellung abgebrochen. Zugriff auf Repository blockiert&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ntfy.sh/schallberts-topic&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-08-20_ntfytest.jpg" alt="Image: First ntfy test on my phone app sent with curl"&gt;&lt;/figure&gt;&#10;Ich installiere die entsprechende App auf meinem Handy, registriere mich auf das &amp;ldquo;Topic&amp;rdquo; und sende die Nachricht ab. Es ist schön, wenn Dinge einfach funktionieren!&#10;Aha, die App zeigt den Zeitpunkt des Einganges an. Das ist mir genau genug.&lt;/p&gt;&#10;&lt;h2 id="durchführung"&gt;Durchführung&lt;/h2&gt;&#10;&lt;p&gt;Ich nehme mir hier nacheinander alle Dienste vor, für die ich gern Benachrichtigungen einrichten möchte.&lt;/p&gt;&#10;&lt;h3 id="borgmatic-1"&gt;&lt;em&gt;borgmatic&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Starten wir mit einem Pilotversuch wieder in kleinen Schritten. Als Erstes konfiguriere ich Borgmatic zum Senden einer Nachricht über &lt;em&gt;Apprise&lt;/em&gt; an &lt;em&gt;ntfy&lt;/em&gt; bei Fehlschlag der Backup-Erstellung:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /borgmatic.d/config.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;on_error&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;echo &amp;#34;Error while creating a backup.&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;apprise -vv --title &amp;#34;Borgmatic Error&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --&lt;span style="color:#ae81ff"&gt;body &amp;#34;Could not run {output}. Aborted {error}.&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;ntfy://schallberts-topic&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dann teste ich das Kommando, indem ich innerhalb des Borgmatic-Containers &lt;em&gt;apprise&lt;/em&gt; anspreche. Und tatsächlich, es funktioniert. Aber der Weg hierhin hat mich locker eine Stunde gekostet, da die &lt;code&gt;yml&lt;/code&gt; mit ihren Syntaxregeln sogar innerhalb von Strings interpretiert und &lt;em&gt;borgmatic&lt;/em&gt; aufgrund von &lt;code&gt;:&lt;/code&gt; sowie &lt;code&gt;-&lt;/code&gt; Zeichen ständig die Konfigurationsdatei nicht lesen wollte. Wenn ihr einen Fehler sieht ähnlich diesem hier:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;At &amp;#39;on_error[1]&amp;#39;: {&amp;#39;apprise -vv --title &amp;#34;Borgmatic Error&amp;#34; --body &amp;#34;Could not run {output}&amp;#39;: &amp;#39;Aborted {error}.&amp;#34; ntfy://schallberts-topic&amp;#39;} is not of type &amp;#39;string&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Daran ist zu erkennen, dass die Interpretation von Zeichen oder Einrückungen schiefgelaufen ist und die Zeichensetzung zu prüfen ist. Alternativ kann auch der Pipe-Operator &lt;code&gt;|&lt;/code&gt; verwendet werden, um ein Kommando zu kombinieren. Referenz: &lt;a href="https://yaml.org/spec/1.2-old/spec.html#id2795688" target="_blank" rel="noopener noreferrer" class="external-link"&gt;yml-Spezifikation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="server-1"&gt;Server&lt;/h3&gt;&#10;&lt;p&gt;Eine Anwendung von &lt;em&gt;ntfy&lt;/em&gt; zur Überwachung von Logins auf einem Server findet sich bereits in der &lt;a href="https://docs.ntfy.sh/examples/#ssh-login-alerts" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dokumentation von &lt;em&gt;ntfy&lt;/em&gt; selbst&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Die Beschreibung zeigt, dass so etwas leicht selbst umzusetzen ist und gibt unter Verwendung von Pluggable Authentication Modules (&lt;a href="https://en.wikipedia.org/wiki/Linux_PAM" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PAM-Bibliothek&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) ein tolles Beispiel.&lt;/p&gt;&#10;&lt;p&gt;Am Ende der Datei &lt;code&gt;sshd&lt;/code&gt; im Verzeichnis &lt;code&gt;etc/pam.d&lt;/code&gt; folgenden Code einfügen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;session optional pam_exec.so /usr/bin/ntfy-ssh-login.sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diese Zeile sagt &lt;em&gt;PAM&lt;/em&gt;, dass sie bei Öffnen einer &lt;code&gt;session&lt;/code&gt; per &lt;code&gt;ssh&lt;/code&gt; das ausführende Modul &lt;code&gt;pam_exec&lt;/code&gt; aufrufen soll, welches dann das hernach angegebene Skript abspult. Der Wert &lt;code&gt;optional&lt;/code&gt; bedeutet, dass die Konfigurationsdatei auch bei Fehlschlag der Aktion weiter durchlaufen werden soll. Mehr Details zum Umgang mit &lt;em&gt;PAM&lt;/em&gt; gibt es z.B. auf &lt;a href="https://www.baeldung.com/linux/pam-ssh-login-notifications" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Baeldung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Anschließend ist im Skript &lt;code&gt;ntfy-ssh-login.sh&lt;/code&gt; schlicht der &lt;em&gt;ntfy&lt;/em&gt;-Aufruf zu hinterlegen, wenn das Skript feststellt, dass PAM ein &lt;code&gt;open_session&lt;/code&gt; Event festgestellt hat.&lt;/p&gt;&#10;&lt;p&gt;Genau so habe ich es bei mir umgesetzt und es funktioniert auf Anhieb. Klasse! Der einzige Nachteil: Diese Modifikation habe ich direkt auf dem Server gemacht. Ohne Container und außerhalb meiner Konfigurations-Sicherung. Wenn ich den Server jetzt aus irgendeinem Grund neu aufsetzen muss, ist die Änderung im &lt;em&gt;PAM&lt;/em&gt; futsch und ich bekomme keine Benachrichtigungen mehr, bis ich die Änderung manuell erneut einpflege.&lt;/p&gt;&#10;&lt;h3 id="server-konfiguration"&gt;Server-Konfiguration&lt;/h3&gt;&#10;&lt;p&gt;Für das Senden eines Berichts nach Ausführen der Konfigurationsautomation muss ich eigentlich nur das im Rahmen des letzten Artikel verfasste &lt;a href="https://blog.schallbert.de/server-config-deploy/#die-fertige-automation"&gt;server-config-action&lt;/a&gt; Skript anpassen.&lt;/p&gt;&#10;&lt;p&gt;Zunächst möchte ich informiert werden, wenn das Skript fehlerfrei durchlief. Dafür füge ich am Ende der Datei einen &lt;em&gt;curl&lt;/em&gt;-Befehl ein.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# server-config-action.sh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# action commands...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# send success notification&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Title: server-config-action&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Priority: low&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Tags: white_check_mark&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -d &lt;span style="color:#e6db74"&gt;&amp;#34;Rollout successful&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ntfy.sh/schallbert-server-config-push-topic&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Sollte das Skript allerdings nicht fehlerfrei durchlaufen, so möchte ich folgendes tun:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Aktionen nach Auftreten des Fehlers abbrechen&lt;/li&gt;&#10;&lt;li&gt;Ein Fehler-Log erstellen&lt;/li&gt;&#10;&lt;li&gt;Dieses Log versenden&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Den ersten Punkt kann ich durch Hinzufügen einer Falle (Englisch &amp;ldquo;Trap&amp;rdquo;) für Fehler erreichen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# server-config-action.sh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;trap &lt;span style="color:#e6db74"&gt;&amp;#39;handle_error $LINENO&amp;#39;&lt;/span&gt; ERR&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# action commands...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hiermit wird die Funktion &lt;code&gt;handle_error&lt;/code&gt; aufgerufen. Ihr wird die Zeilennummer mitgegeben, an der der Fehler &lt;code&gt;ERR&lt;/code&gt; auftrat. Damit decke ich auch Fehler ab, die beim Neustart der Container entstehen können. Das Fehler-Log erstelle ich mir durch Wegschreiben der Ausgabe der einzelnen Skriptbefehle. Dies erreiche ich mit der folgenden Zeile:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# server-config-action.sh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;exec 3&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; 1&amp;gt;server-config-action.log 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# action commands ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Jeglicher Standard-Output &lt;code&gt;stdout&lt;/code&gt; soll mit diesem Befehl an den Dateideskriptor &lt;code&gt;3&lt;/code&gt;, hier angegeben mit der Logdatai &lt;code&gt;server-config-action-log&lt;/code&gt;, überschreibend weitergereicht werden. Für &amp;ldquo;Anhängen&amp;rdquo; müssten es zwei Umleitungsoperatoren &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt; sein. Somit ersetzt die Logdatei die Konsolenausgabe, welche ich im vorigen Artikel noch an dieser Stelle hatte.&lt;/p&gt;&#10;&lt;p&gt;Um das Log zugesandt zu bekommen, definiere ich nun folgende Funktion am Anfang des Bash-Skripts:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# server-config-action.sh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# on error, send a notification&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;handle_error&lt;span style="color:#f92672"&gt;()&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# stop redirecting to file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; exec 1&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; 1&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; curl &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Title: server-config-action FAILED&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Priority: high&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Tags: x&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -T server-config-action.log &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Filename: server-config-action.log&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ntfy.sh/schallbert-server-config-push-topic&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; exit &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# set error trap&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# redirect stdout and stderr to file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# action commands...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-08-20_ntfytest_allchannels.jpg" alt="Image: Phone screenshot of my ntfy messages"&gt;&lt;/figure&gt;&#10;&lt;h3 id="gitea-1"&gt;&lt;em&gt;Gitea&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Für Berichte von Github Actions gibt es bereits ein existierendes Beispiel auf &lt;a href="https://docs.ntfy.sh/examples/#github-actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ntfy_examples/#github-actions&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Und das Beste: Da der &lt;em&gt;act_runner&lt;/em&gt; von &lt;em&gt;Gitea&lt;/em&gt; an den wesentlichen Stellen &lt;a href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables" target="_blank" rel="noopener noreferrer" class="external-link"&gt;in den Umgebungsparametern&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kompatibel ist, funktioniert das Senden an &lt;em&gt;ntfy&lt;/em&gt; bei mir auf Anhieb. In der Workflow-Datei des runners muss nur der auf der Website angegebene &lt;em&gt;curl&lt;/em&gt;-Befehl angegeben werden und fertig.&lt;/p&gt;&#10;&lt;h2 id="ergebnis"&gt;Ergebnis&lt;/h2&gt;&#10;&lt;p&gt;Hiermit sind jetzt fünf Benachrichtigungen über die wichtigsten Vorgänge auf meinem Server eingerichtet. Ich habe die zugrundeliegenden Mechaniken verstanden und kann jederzeit weitere Benachrichtigungen erstellen, wenn ich sie benötige. Löse ich alle Benachrichtigungen testweise aus, zeigt mein Handy das dargestellte Bild.&lt;/p&gt;&#10;&lt;p&gt;Eine Unabhängigkeit vom zu überwachenden System habe ich noch nicht erreicht. Die Benachrichtigungen gehen sämtlich von der betroffenen Maschine aus und es bestehen zum Teil &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/#reverse-proxy-connection-refused"&gt;sogar Abhängigkeiten&lt;/a&gt; zwischen Docker-Containern. Der &lt;em&gt;act_runner&lt;/em&gt; muss sich zum Beispiel über Websockets mit &lt;em&gt;Gitea&lt;/em&gt; verbinden. Und das funktioniert nur, wenn &lt;em&gt;Caddy&lt;/em&gt; den Reverse Proxy bereitstellt.&lt;/p&gt;&#10;&lt;p&gt;Stelle ich in den nächsten Monaten fest, dass ein Dienst &amp;ldquo;unter meinem Radar&amp;rdquo; nicht mehr arbeitsfähig ist, werde ich diese Unabhängigkeit herstellen müssen und entsprechend darüber berichten.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Kontext zum Setup mit &lt;a href="https://docs.ntfy.sh/#getting-started" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ntfy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;Kontext zur &lt;a href="https://github.com/caronc/apprise/wiki/Notify_signal" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Signal-API&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;Bericht von &lt;code&gt;asad-awadia&lt;/code&gt; zur &lt;a href="https://blog.aawadia.dev/2023/04/24/signal-api/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Einrichtung der Signal-API&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:4"&gt;&#10;&lt;p&gt;Healthchecks ist direkt verfügbar als &lt;a href="https://hub.docker.com/r/healthchecks/healthchecks" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker-Image&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:5"&gt;&#10;&lt;p&gt;Übrigens sind die Operatoren &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; und &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; für mich nicht schwer zu merken, denn früher habe ich viel mit der Programmiersprache &lt;code&gt;C&lt;/code&gt; zu tun gehabt. Dort - und in vielen anderen Programmiersprachen auch - sind dies Shift-Operatoren, die einen Wert bitweise &amp;ldquo;verschieben&amp;rdquo; bzw. ein Feld auf ein anderes schieben können. Bei den &lt;a href="https://en.wikipedia.org/wiki/Reduced_instruction_set_computer" target="_blank" rel="noopener noreferrer" class="external-link"&gt;RISC&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Architekturen, die ich damals verwendet habe, war die Schiebe-Operation &amp;ldquo;billig&amp;rdquo;, also sehr schnell und speicherplatzschonend umgesetzt.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Server-Konfiguration ausrollen</title><link>https://blog.schallbert.de/server-config-deploy/</link><pubDate>Wed, 31 Jul 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/server-config-deploy/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-07-31-server-configdeploy-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Diagram showing a config file rollout on Schallbert&amp;#39;&amp;#39;s server"&#10; title="Server-Konfiguration ausrollen" /&gt;&#10;&lt;p&gt;Im vorigen Artikel habe ich meine &lt;a href="https://blog.schallbert.de/server-config-version-control/"&gt;Konfigurationsdateien unter Versionskontrolle gebracht&lt;/a&gt;. Nun will ich die auf dem Server bereitgestellten Updates automatisiert einspielen.&lt;/p&gt;&#10;&lt;h2 id="worum-geht-es-hier"&gt;Worum geht es hier?&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schreiben eines Skripts zur automatischen Erkennung des Update-Triggers&lt;/li&gt;&#10;&lt;li&gt;Server-Anwendungen sollen heruntergefahren und eine Sicherungskopie erstellt werden&lt;/li&gt;&#10;&lt;li&gt;Das Skript soll die Konfiguration auf dem System verteilen&lt;/li&gt;&#10;&lt;li&gt;Anschließend sollen alle Anwendungen neu gestartet werden&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="wege-und-möglichkeiten"&gt;Wege und Möglichkeiten&lt;/h2&gt;&#10;&lt;p&gt;Auch hier habe ich wieder mehrere Stunden mit Recherche verbracht. Bei größeren Projekten verwenden Infrastrukturexperten anscheinend spezialisierte Automatisierungswerkzeuge. Dazu zählen &lt;a href="https://docs.ansible.com/ansible/latest/getting_started/index.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Ansible&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, einfachere Tools wie &lt;a href="https://www.cdi.st/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cdist&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; oder gar &lt;a href="https://kubernetes.io/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Kubernetes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für stark skalierende Dienste.&lt;/p&gt;&#10;&lt;h3 id="automatisierungsdienste-ansible-cdist-kubernetes"&gt;Automatisierungsdienste Ansible, cdist, Kubernetes&lt;/h3&gt;&#10;&lt;p&gt;Ansible und cdist scheinen dabei ein ähnliches Konzept zu verfolgen: Auf der Quellmaschine (in diesem Beispiel meinem Laptop) erstelle ich die Konfiguration für meinen Server und lege diese sowie die Vorschrift zur Konfiguration meiner Dienste in einem &amp;ldquo;Playbook&amp;rdquo; (Ansible) bzw. in &amp;ldquo;types&amp;rdquo; (cdist) ab.&lt;/p&gt;&#10;&lt;p&gt;Vereinfacht gesagt - so wie ich das verstanden habe - kümmert sich das Werkzeug anschließend auf Knopfdruck darum, die Konfiguration zu bauen, sich per &lt;code&gt;ssh&lt;/code&gt; auf dem Zielhost einzuwählen, sie dort hinüberzuschieben und anschließend zu starten. Für Ansible existieren sogar für mein Szenario mit &lt;code&gt;docker-compose&lt;/code&gt; &lt;a href="https://medium.com/swlh/deploying-docker-compose-applications-with-ansible-and-github-actions-7f1740392507" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Tutorials wie dieses&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, was den Einstieg nochmal erleichtert.&lt;/p&gt;&#10;&lt;p&gt;Einen anderen Weg geht Kubernetes, was sich als eher als Containermanager, Loadbalancer und Skalierungsagent versteht, aber für meine Zwecke Ähnliches leisten kann.&lt;/p&gt;&#10;&lt;h3 id="mein-weg"&gt;Mein Weg&lt;/h3&gt;&#10;&lt;p&gt;Ich hingegen brauche von den Fähigkeiten dieser Programme nur einen Bruchteil. Zudem schrecken mich der &amp;ldquo;zusätzliche&amp;rdquo; &lt;code&gt;ssh&lt;/code&gt;-Kanal, der Konfigurationsaufwand, die teils auf dem Zielsystem erforderlichen, zusätzlichen Programme sowie das erforderliche Einlesen und Auswählen des für mich besten Werkzeuges ab. Denn dank meiner sehr einfachen Pipeline aus dem letzten Artikel befindet sich die Konfiguration bereits auf meinem Server. Sie muss &amp;ldquo;nur noch&amp;rdquo; an die richtigen Stellen kopiert und betroffene Services einem Neustart unterzogen werden.&lt;/p&gt;&#10;&lt;p&gt;Daher versuche ich, dieses Problem mit Bordwerkzeug, meinem Gehirn in Betriebsbereitschaft und ein paar Suchanfragen bei Einschlägigen Foren zu den Themen &lt;a href="https://superuser.com/questions/181517/how-to-execute-a-command-whenever-a-file-changes" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Ausführen eines Skriptes bei Dateiänderung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://www.freecodecamp.org/news/copy-a-directory-in-linux-how-to-cp-a-folder-in-the-command-line-in-linux-and-unix-macos/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Kopieren von Ordnerstrukturen unter Linux&amp;rdquo;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zu lösen.&lt;/p&gt;&#10;&lt;h3 id="risiko-zirkelbezug"&gt;Risiko Zirkelbezug&lt;/h3&gt;&#10;&lt;p&gt;Ein Risiko gehe ich dabei jedoch ein: Da das Deployment über Gitea läuft, die Konfiguration aber Gitea selbst betrifft, kann ich bei einem Fehler in diesem Modul nichts mehr um- oder zurückstellen: Der Gitea-Service ist dann ja kaputt. In diesem Szenario müsste ich also händisch mit auf dem Server die Konfiguration wieder ans Laufen bekommen.&lt;/p&gt;&#10;&lt;p&gt;Ich probiere es jetzt trotzdem aus und schaue, ob ich tatsächlich mit so einem Problem konfrontiert werde. Falls ja, steige ich halt auf &lt;code&gt;cdist&lt;/code&gt; um und dokumentiere dies in einem eigenen Artikel! 🤗&lt;/p&gt;&#10;&lt;h2 id="vorbereitung-ordnersystem-und-skripte-erstellen"&gt;Vorbereitung: Ordnersystem und Skripte erstellen&lt;/h2&gt;&#10;&lt;p&gt;Ich überlege kurz und erstelle ein paar Ordner im Server-Dateisystem:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /opt/server-config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir automation-hooks-trigger&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir automation-hooks-handler&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diese Ordner sollen die zwei Seiten der Automation abbilden. &lt;code&gt;trigger&lt;/code&gt; enthält Textdateien, die von der (Web)-Serviceseite aus manipuliert werden können. So soll &lt;em&gt;act_runner&lt;/em&gt; beispielsweise die Datei &lt;code&gt;server-config-update&lt;/code&gt; schreiben, sobald ein Update vorliegt.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;service &amp;ndash;&amp;gt; writes to trigger file ||| server_handler() &amp;ndash;&amp;gt; trigger_file.changed ? run_action() : loop()&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Im Handler-Ordner wiederum befinden sich Skriptdateien, die die erforderlichen Änderungen auf dem Server-Dateisystem vornehmen.&lt;/p&gt;&#10;&lt;h2 id="durchführung-shell-script-zum-abhandeln-des-updates"&gt;Durchführung: Shell-Script zum Abhandeln des Updates&lt;/h2&gt;&#10;&lt;p&gt;Im vorigen Artikel habe ich im Container &lt;em&gt;act_runner&lt;/em&gt; zum Signalisieren des Vorhandenseins einer neuen Serverkonfiguration das Kommando &lt;code&gt;touch server-config-update.txt&lt;/code&gt; ausgeführt. Auf dem Server prüfe ich nun mit folgendem Code periodisch ab, ob sich diese Datei geändert hat.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### set directories&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;actionfile&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/opt/server-config/automation-hooks-handler/server-config-action.sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;triggerfile&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/opt/server-config/automation-hooks-trigger/server-config-update.txt&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### Set initial time of file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;LTIME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;stat -c %Z &lt;span style="color:#e6db74"&gt;${&lt;/span&gt;triggerfile&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;while&lt;/span&gt; true&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ATIME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;stat -c %Z &lt;span style="color:#e6db74"&gt;${&lt;/span&gt;triggerfile&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[[&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$ATIME&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; !&lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$LTIME&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;]]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;${&lt;/span&gt;actionfile&lt;span style="color:#e6db74"&gt;}&lt;/span&gt; 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; LTIME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;$ATIME&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sleep &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Per &lt;a href="https://en.wikipedia.org/wiki/Shebang_%28Unix%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;#!/bin/bash&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; wird dem Betriebssystem der Interpreter genannt, mit dem das Skript auszuführen ist. Der Befehl &lt;a href="https://wiki.ubuntuusers.de/stat/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;stat -c %Z&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; fragt ab, wann die Datei zuletzt geändert wurde und gibt den Zeitpunkt im &lt;a href="https://www.epochconverter.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Epoch-Format&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zurück. Schließlich wird die zu Anfang des Skriptes mit der in der Schleife genommenen Zeit verglichen und bei Veränderung kann dann die noch zu schreibende Deploy-Routine ablaufen. Anschließend wird der Referenzzeitpunkt aktualisiert.&#10;Zum Schluss pausiert das Skript (&lt;code&gt;sleep 10&lt;/code&gt;) für zehn Sekunden, bevor die Abfrage erneut losläuft. Ich verwende absolute Pfade, da ich das Skript sowohl für Tests von der Konsole aus als auch automatisch per &lt;code&gt;cron&lt;/code&gt; oder &lt;code&gt;systemd&lt;/code&gt; starten können will.&lt;/p&gt;&#10;&lt;h3 id="erster-test-und-einhängen-in-den-autostart"&gt;Erster Test und Einhängen in den &amp;ldquo;Autostart&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Nun müssen wir die Datei für einen ersten Test ausführbar machen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# change file mode bits: add &amp;#34;executable&amp;#34; flag to server-config-handler script&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;chmod +x /opt/server-config/automation-hooks-handler/server-config-handler.sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dann erstelle ich die Datei &lt;code&gt;server-config-action.sh&lt;/code&gt; und gebe ihr als einziges Kommando ein &lt;code&gt;echo&lt;/code&gt;. Schauen wir mal, ob bis hierhin alles funktioniert. Dafür erstelle ich lokal meine Update-Triggerdatei und starte das Skript. Anschließend modifiziere ich sie in einer anderen Shell-Instanz per &lt;code&gt;touch server-config-update.txt&lt;/code&gt; und prompt erscheint in der Konsole &lt;code&gt;--- CONFIG UPDATE TRIGGER detected ---&lt;/code&gt;. Super!&lt;/p&gt;&#10;&lt;p&gt;Auf dem Server später muss ich das Skript automatisch nach einem Neustart ausführen lassen. Dafür nehme ich das Werkzeug &lt;code&gt;cron&lt;/code&gt; zur &lt;a href="https://wiki.ubuntuusers.de/Cron/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hilfe&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und erstelle einen neuen Eintrag per &lt;code&gt;crontab -e&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# cron can automatically execute recurring tasks&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# in this case, we&amp;#39;re running server-config-handler script on reboot&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@reboot sh /opt/server-config/automation-hooks-handler/server-config-handler.sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mit dem Befehl &lt;code&gt;ps aux&lt;/code&gt; kann ich nun prüfen, ob das Skript auch wirklich ausgeführt wird:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;schallbert@server: ps aux&#10;[...]&#10;8:15 0:00 /bin/bash ./server-config-handler.sh&#10;8:15 0:00 sleep 10&#10;[...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="sicherungskopie"&gt;Sicherungskopie&lt;/h2&gt;&#10;&lt;p&gt;Eine Sicherungskopie meiner Anwendungen und Dateien anzufertigen bevor ich das Update ausrolle macht total Sinn. Also teile ich &lt;a href="https://blog.schallbert.de/server-protection/#regelm%c3%a4%c3%9fige-backups"&gt;Borg&lt;/a&gt; mit, dass ich jetzt ein Backup anlegen möchte. Vorher muss ich natürlich alle Services stoppen. Somit sind alle Daten zugriffsfrei, kohärent und statisch.&lt;/p&gt;&#10;&lt;h3 id="zustand-und-daten-einfrieren"&gt;Zustand und Daten einfrieren&lt;/h3&gt;&#10;&lt;p&gt;Hierfür lege ich in &lt;code&gt;automation-hooks-handler&lt;/code&gt; ein Skript an, welches alle Container bis auf &lt;em&gt;borg&lt;/em&gt; selbst beendet.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#! /bin/bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /opt/automation-hooks-handler/backup-pre-action.sh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# this shell script shuts down all docker containers prior to backup&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;Shutting down containers for backup:&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;watchtower...&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd /opt/watchtower&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose down 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Der Ausdruck &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; bedeutet, dass eventuelle Ausgabe von Fehlern in die Konsole umgeleitet wird. Die Zahl &lt;code&gt;1&lt;/code&gt; stellt nämlich den Filedescriptor für &lt;code&gt;stdout&lt;/code&gt; dar, während &lt;code&gt;2&lt;/code&gt; &lt;code&gt;stderr&lt;/code&gt; meint. Der Operator &lt;code&gt;&amp;gt;&amp;amp;&lt;/code&gt; funktioniert als &lt;code&gt;redirect merger&lt;/code&gt;, also als Umleitung und Zusammenführung. Später können wir an diesem Punkt hingehen und die Ausgabe in ein Logfile schreiben - das lasse ich aber der Einfachheit halber erst einmal aus.&lt;/p&gt;&#10;&lt;h3 id="borgmatic-trigger-handler-mechanismus-2-und-3"&gt;Borgmatic: Trigger-Handler Mechanismus #2 und #3&lt;/h3&gt;&#10;&lt;p&gt;Das Skript kann ich nun auf zwei Arten ausführen lassen:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Als Aufruf durch das weiter oben beschriebene Skript &lt;code&gt;server-config-handler&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Durch die vor &lt;em&gt;borg&lt;/em&gt; geschaltete Automatisierungslösung &lt;em&gt;borgmatic&lt;/em&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Ich entscheide mich für die zweite Option und schreibe daher in die &lt;code&gt;borgmatic.d/config.yml&lt;/code&gt; die folgenden Befehle:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# borgmatic.d/config.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# List of one or more shell commands or scripts to execute before&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# creating a backup, run once per repository.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;before_backup&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;echo &amp;#34;Triggering container shutdown for backup.&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;touch /etc/automation-hooks-trigger/backup-pre.txt&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;sleep 20&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;echo &amp;#34;Assuming container shutdown complete. Creating the backup now.&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;after_backup&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;echo &amp;#34;Triggering container restart after backup.&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;touch /etc/automation-hooks-trigger/backup-post.txt&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;sleep 10&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;echo &amp;#34;Assuming container restart complete. Exiting.&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Damit dies richtig funktioniert, muss ich in der zugehörigen &lt;code&gt;docker-compose.yml&lt;/code&gt; natürlich ein Volume anlegen und auf den Pfad im Dateisystem des Servers zeigen lassen: &lt;code&gt;${VOLUME_UPDATE_TRIGGER}:/etc/automation-hooks-trigger&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Nun muss ich die Gegenseite dieser Trigger einrichten: Handler überwachen die Triggerdateien auf Änderungen und rufen entsprechend Action-Skripte auf. Sie sind bis auf die Pfade von &lt;code&gt;triggerfile&lt;/code&gt; und &lt;code&gt;actionfile&lt;/code&gt; identisch mit dem oben erläuterten Handler.&lt;/p&gt;&#10;&lt;h3 id="das-backup-anlegen"&gt;Das Backup anlegen&lt;/h3&gt;&#10;&lt;p&gt;Würde ich &lt;em&gt;borg&lt;/em&gt; direkt ansprechen, so ließe sich das Backup über &lt;code&gt;create&lt;/code&gt; erstellen. Dazu müsste ich angeben, in welchem Repository die Sicherungskopie abgelegt werden soll und unter welchem Namen. Im Beispiel unten wird das mit dem Scope-Operator vorgegeben: &lt;code&gt;::config-update&lt;/code&gt;. Anschließend werden die zu sichernden Ordner angegeben.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;borg create /path/to/repo::config-update ~/opt&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich verwende ja &lt;em&gt;borgmatic&lt;/em&gt;, was mir über die Konfigurationsdatei einiges abnimmt. Dafür muss ich den Befehl allerdings im Container ausführen. Um besser prüfen zu können, ob auch alles funktioniert, lasse ich mir Statistiken &amp;ldquo;verbose&amp;rdquo; auf die Konsole ausgeben (&lt;code&gt;--stats -v 1&lt;/code&gt;) sowie die kopierten Dateien anzeigen &lt;code&gt;--files&lt;/code&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker exec borgmatic sh -c &lt;span style="color:#e6db74"&gt;&amp;#34;cd &amp;amp;&amp;amp; borgmatic --stats -v 1 --files 2&amp;gt;&amp;amp;1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diese Zeile füge ich dem Automationsskript hinzu.&lt;/p&gt;&#10;&lt;h2 id="zweiter-test-zum-anlegen-des-backups"&gt;Zweiter Test zum Anlegen des Backups&lt;/h2&gt;&#10;&lt;p&gt;Wenn jetzt alles funktioniert, sieht der komplette Ablauf wie folgt aus:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Nach Eingang der geänderten Dateien durch Giteas Automation legt &lt;em&gt;act_runner&lt;/em&gt; (im Docker-Container) die Dateien auf dem Server ab und setzt anschließend den &lt;code&gt;server-config-update&lt;/code&gt; Trigger.&lt;/li&gt;&#10;&lt;li&gt;Binnen &lt;code&gt;10sec&lt;/code&gt; wird der Trigger von &lt;code&gt;server-config-handler.sh&lt;/code&gt; erkannt, welches anschließend &lt;code&gt;server-config-action.sh&lt;/code&gt; aufruft.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;borgmatic&lt;/em&gt; (im Docker-Container) wird darin beauftragt, ein Backup anzufertigen. Dieses wiederum schreibt den &lt;code&gt;pre-backup&lt;/code&gt; Trigger.&lt;/li&gt;&#10;&lt;li&gt;Erneut binnen &lt;code&gt;10sec&lt;/code&gt; wird durch diesen &lt;code&gt;pre-backup-handler.sh&lt;/code&gt; das &lt;code&gt;backup-pre-action.sh&lt;/code&gt; Skript aufgerufen und alle Container bis auf &lt;em&gt;borgmatic&lt;/em&gt; gestoppt.&lt;/li&gt;&#10;&lt;li&gt;Durch die eingebaute Verzögerung wartet &lt;em&gt;borgmatic&lt;/em&gt; dies ab und erstellt nun das Backup.&lt;/li&gt;&#10;&lt;li&gt;Nach dem Backup schreibt &lt;em&gt;borgmatic&lt;/em&gt; den &lt;code&gt;backup-post-action.sh&lt;/code&gt; Trigger.&lt;/li&gt;&#10;&lt;li&gt;Binnen weiterer &lt;code&gt;10sec&lt;/code&gt; erkennt &lt;code&gt;post-backup-handler.sh&lt;/code&gt; die geänderte Datei und fährt alle Container über &lt;code&gt;post-backup-actions.sh&lt;/code&gt; wieder hoch.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;borgmatic&lt;/em&gt; teilt &lt;code&gt;server-config-update.sh&lt;/code&gt; mit, ob irgendwo im Prozess bis jetzt Fehler auftraten. Falls nicht, wird fortgefahren.&lt;/li&gt;&#10;&lt;li&gt;Alle Docker-Container werden gestoppt.&lt;/li&gt;&#10;&lt;li&gt;Die Server-Konfiguration wird auf die entsprechenden Stellen ausgerollt.&lt;/li&gt;&#10;&lt;li&gt;Es findet ein Neustart aller Docker-Container mit neuer Konfiguration statt.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Ein erster Erfolg: Bis Punkt 6 laufen diese Skripte sogar auf meinem Laptop bereits durch:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;schallbert@laptop: touch server-config-update.txt&#10;server-config-handler@laptop: --- RUN backup ---&#10;borgmatic@docker: /etc/borgmatic.d/config.yml: Running 4 commands for pre-backup hook&#10; Triggering container shutdown for backup. &#10;backup-pre-handler.sh@laptop: --- RUN backup-pre-action.sh ---&#10; Shutting down containers for backup:&#10; watchtower...&#10; [...]&#10; complete.&#10;borgmatic@docker: Assuming container shutdown complete. Creating the backup now.&#10; local: Creating archive&#10; Failed to create/acquire the lock /mnt/repository/lock.exclusive&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="fehlerbehebung-für-failed-to-acquire-the-lock"&gt;Fehlerbehebung für &amp;ldquo;failed to acquire the lock&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Dieses Problem taucht bei mir dann auf, wenn &lt;em&gt;borg&lt;/em&gt; beim Erstellen eines Backups einen Fehler meldet, der das Programm zum Abbruch bringt. In diesem Falle wird das Repository anscheinend nicht korrekt freigegeben, sodass es nach Neustart des Containers für den alten, nun nicht mehr existierenden Container reserviert bleibt. Das folgende Kommando löst dieses Problem:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker exec borgmatic sh -c &lt;span style="color:#e6db74"&gt;&amp;#34;cd &amp;amp;&amp;amp; borg break-lock /mnt/repository&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="übertragen-der-konfiguration"&gt;Übertragen der Konfiguration&lt;/h3&gt;&#10;&lt;p&gt;Nun müssen die Konfigurationsdateien noch an die jeweils richtige Stelle auf dem Server kopiert werden. Glücklicherweise hatte ich die Ziel-Ordnerstruktur bereits beim Erstellen des Repository geklont, sodass ich das mit einem einzigen Kopierbefehl ohne &amp;ldquo;hardcoding&amp;rdquo; hinbekommen sollte. Nach ein paar Netzrecherchen und einem Blick in die Bedienungsanleitung für den Kopierbefehl &lt;code&gt;man cp&lt;/code&gt; habe ich mein Kommando:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# copy recursively contents of folder &amp;#34;server-config&amp;#34; to &amp;#34;/opt&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo cp -r -v /opt/server-config/. /opt 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Somit sage ich dem Betriebssystem, es solle den Inhalt (&lt;code&gt;/.&lt;/code&gt;) des Ordners &lt;code&gt;server-config&lt;/code&gt; rekursiv (&lt;code&gt;-r&lt;/code&gt;), also inklusive aller Unterordner, in den Ordner &lt;code&gt;opt&lt;/code&gt; befördern, welcher sich im Stammverzeichnis &lt;code&gt;/&lt;/code&gt; befindet. &lt;code&gt;cp&lt;/code&gt; arbeitet hierbei überschreibend-ergänzend, wird also noch nicht vorhandene Dateien erstellen und vorhandene überschreiben, sie also nicht unter gleichem Namen &amp;ldquo;daneben kopieren&amp;rdquo;. Mit der Option &lt;code&gt;-v&lt;/code&gt; lasse ich zusätzliche Details ausgeben, und mit &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; leite ich die Fehlerausgabe in die Konsole um.&lt;/p&gt;&#10;&lt;p&gt;Alle Ordnerindikatoren müssen sich genau dort befinden, wo sie im Befehl stehen: Ein Slash hinter &lt;code&gt;/opt/&lt;/code&gt; würde Ordner nicht-überschreibend redundant kopieren, Dateien aber überschreiben. Ohne &lt;code&gt;.&lt;/code&gt; würde der Ordner &lt;code&gt;server-config&lt;/code&gt; am Zielpfad erstellt.&lt;/p&gt;&#10;&lt;h3 id="umstieg-auf-rsync"&gt;Umstieg auf rsync&lt;/h3&gt;&#10;&lt;p&gt;Der &lt;code&gt;cp&lt;/code&gt;-Befehl kopiert leider auch ein paar Dateien, die ich gar nicht kopiert haben möchte: Repository-spezifische Ordner wie &lt;code&gt;.gitea&lt;/code&gt;, oder auch die Ordner für Trigger und Handler. Diese benötige ich nur unter &lt;code&gt;server-config&lt;/code&gt;, nicht aber auch direkt in &lt;code&gt;opt&lt;/code&gt;. Um dies zu beheben nutze ich stattdessen das Kommando &lt;code&gt;rsync&lt;/code&gt;. Dort kann ich mit &lt;code&gt;-u&lt;/code&gt; einstellen, dass nur neuere Dateien überschrieben werden sollen und über eine &lt;code&gt;--exclude&lt;/code&gt;-Option nicht zu kopierende Dateien und Ordner ausschließen. Dies sieht dann so aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -r -u -v --exclude &lt;span style="color:#e6db74"&gt;&amp;#39;.*&amp;#39;&lt;/span&gt; --exclude &lt;span style="color:#e6db74"&gt;&amp;#39;README.md&amp;#39;&lt;/span&gt; --exclude &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;otherFolders&amp;gt;&amp;#39;&lt;/span&gt; /opt/server-config/. /opt 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Doch plötzlich startet &lt;em&gt;gitea&lt;/em&gt; nicht mehr. Fehlermeldung:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;docker@server: [...] failed to load config file &amp;#34;app.ini&amp;#34;: open: permission denied&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Nach längerem Grübeln und diversen Neustarts des Docker-Clients sehe ich, dass &lt;em&gt;rsync&lt;/em&gt; die Berechtigungen der Quelldatei auf die Zieldatei geschrieben hat, was bei &lt;em&gt;cp&lt;/em&gt; vorher nicht der Fall war: &lt;code&gt;-rw-------&lt;/code&gt;. Nun ändere ich dies, indem ich &lt;code&gt;chmod +r app.ini&lt;/code&gt; ausführe. Schon fährt alles wieder wie gewohnt hoch! 🎉&lt;/p&gt;&#10;&lt;h3 id="alle-anwendungen-neu-starten"&gt;Alle Anwendungen neu starten&lt;/h3&gt;&#10;&lt;p&gt;Da ich alles auf meinem Server in Docker betreibe, genügen hierfür zwei einfache &lt;a href="https://docs.docker.com/reference/cli/docker/container/restart/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Befehle&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker stop &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;docker ps -a -q&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...roll out config changes...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker restart &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;docker ps -a -q&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="die-fertige-automation"&gt;Die fertige Automation&lt;/h2&gt;&#10;&lt;p&gt;Mein Skript ist nun fertig und ruft lediglich das Action-Skript auf.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#! /bin/bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#/opt/automation-hooks-handler/server-config-handler.sh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[[&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$ATIME&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; !&lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$LTIME&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;]]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;--- CONFIG UPDATE TRIGGER detected ---&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ./server-config-action.sh 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; LTIME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;$ATIME&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sleep &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das Skript &lt;code&gt;server-config-action&lt;/code&gt; führt dann die oben beschriebenen Aktionen aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#! /bin/bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#/opt/automation-hooks-handler/server-config-action.sh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;--- RUN backup ---&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker exec borgmatic sh -c &lt;span style="color:#e6db74"&gt;&amp;#34;cd &amp;amp;&amp;amp; borgmatic --stats -v 1 --files 2&amp;gt;&amp;amp;1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;--- STOP all containers ---&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker stop &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;docker ps -a -q&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;--- DEPLOY config ---&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cp -r -v /opt/server-config/. /opt 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;--- RESTART all containers ---&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker restart &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;docker ps -a -q&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Damit das Ganze nun zuverlässig funktioniert, müssen die drei Handler-Skripte im Hintergrund laufen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;server-config-handler.sh&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;backup-pre-handler.sh&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;backup-post-handler.sh&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Sie reagieren auf die jeweiligen Trigger, ausgelöst durch &lt;em&gt;act_runner&lt;/em&gt; von Gitea bzw. durch &lt;em&gt;borgmatic&lt;/em&gt;. Ich erweitere das &lt;em&gt;crontab&lt;/em&gt; jetzt entsprechend und bin dann mit der Aufgabe vorerst fertig. Die Konsolenausgabe des gesamten Vorgangs sieht so aus:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;--- RUN backup ---&#10;/etc/borgmatic.d/config.yml: Running 4 commands for pre-backup hook&#10;Triggering container shutdown for backup.&#10;--- RUN backup-pre-action.sh ---&#10;Shutting down containers for backup:&#10;gitea... &#10;fail2ban...&#10;complete.&#10;Assuming container shutdown complete. Creating the backup now.&#10;local: Creating archive&#10;&amp;lt;borg archive stats&amp;gt;&#10;/etc/borgmatic.d/config.yml: Running 4 commands for post-backup hook&#10;Triggering container restart after backup.&#10;--- RUN backup-post-action.sh ---&#10;Restarting containers after backup:&#10;fail2ban...&#10;gitea... &#10;watchtower...&#10;complete.&#10;Assuming container restart complete. Exiting.&#10;local: Pruning archives&#10;local: Compacting segments&#10;compaction freed about 1.82 MB repository space.&#10;local: Running consistency checks&#10;summary:&#10;/etc/borgmatic.d/config.yml: Successfully ran configuration file&#10;--- STOP all containers ---&#10;&amp;lt;container ids&amp;gt;&#10;--- DEPLOY config ---&#10;sending incremental file list&#10;&amp;lt;files that are copied&amp;gt;&#10;sent 206,558 bytes received 3,463 bytes 420,042.00 bytes/sec&#10;total size is 193,167 speedup is 0.92&#10;--- RESTART all containers ---&#10;&amp;lt;container ids&amp;gt;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Prima! Nun fehlt mir nur noch, dass diese Ausgabe mir zugestellt wird, falls etwas bei diesem Vorgang schief geht.&lt;/p&gt;&#10;</description></item><item><title>Server-Konfiguration mit Git</title><link>https://blog.schallbert.de/server-config-version-control/</link><pubDate>Mon, 15 Jul 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/server-config-version-control/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-07-15-server-versioncontrol-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Diagram thumb for putting server config under version control"&#10; title="Server-Konfiguration mit Git" /&gt;&#10;&lt;h2 id="worum-geht-es"&gt;Worum geht es?&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Ich wünsche eine Versionskontrolle für die Konfiguration von Serverapplikationen&lt;/li&gt;&#10;&lt;li&gt;Diskussion technischer Lösungen zur Umsetzung&lt;/li&gt;&#10;&lt;li&gt;Tutorial: Docker/Gitea/andere &amp;ldquo;Secrets&amp;rdquo; von der Versionskontrolle ausnehmen&lt;/li&gt;&#10;&lt;li&gt;Tutorial: Deploy-Pipeline erstellen&lt;/li&gt;&#10;&lt;li&gt;Automationstrigger für nachgelagertes Ausrollen der Dateien auf dem Server programmieren&lt;/li&gt;&#10;&lt;li&gt;Nächster Artikel: Automation auf dem Server integrieren und Upgrades einspielen&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="hintergrund"&gt;Hintergrund&lt;/h2&gt;&#10;&lt;p&gt;Ich habe auf &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/"&gt;meinem Server&lt;/a&gt; ja &lt;a href="https://blog.schallbert.de/server-auto-upgrade/"&gt;Watchtower laufen&lt;/a&gt;, um die Installierten Distributionen automatisch auf Stand und frisch gepatcht zu halten. Vor Kurzem hatte ich nun den Fall, dass sich mein &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/"&gt;Gitea Action Runner&lt;/a&gt; nicht mehr automatisch starten lassen wollte und auch bei manuellem Start eine Fehlermeldung ausgab.&lt;/p&gt;&#10;&lt;p&gt;Anscheinend hatte sich Docker ohne meine Aufsicht &lt;a href="https://blog.schallbert.de/server-auto-upgrade/"&gt;auf Stand gebracht&lt;/a&gt; und warf nun beim Hochfahren des Runner-Containers einen Volumefehler, die ich so vorher noch nicht gesehen hatte. Die Fehlermeldung war eindeutig und ließ sich mittels kleiner Änderungen in einer Konfigurationsdatei für Gitea leicht beheben. Dennoch hatte ich nun das Gefühl, dass eine Versionskontrolle für meine Konfiguration sinnvoll wäre, um Änderungen, Updates und ihre Beweggründe später besser nachvollziehen zu können.&lt;/p&gt;&#10;&lt;h2 id="vorüberlegungen"&gt;Vorüberlegungen&lt;/h2&gt;&#10;&lt;p&gt;Klingt nun für mich nach einem Zirkelbezug: Ich nehme die Konfigurationsdateien für meinen Server in Gitea auf, welches selbst auf meinem Server läuft. Das könnte beim Auto-Deploy noch spannend werden. Aber mehr dazu später.&lt;/p&gt;&#10;&lt;p&gt;Spontan fallen mir zwei Möglichkeiten ein, eine Versionskontrolle mit automatischer Synchronisation zu bekommen:&lt;/p&gt;&#10;&lt;h3 id="1-hardlink"&gt;1. Hardlink&lt;/h3&gt;&#10;&lt;p&gt;Diese Lösung würde die auf meinem Server in vielen Ordnern verstreuten Konfigurationdateien per &lt;a href="https://en.wikipedia.org/wiki/Hard_link" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hardlink&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; in einen zum Repository erklärten Ordner ablegen. Warum per Hardlink? Weil jeder Service auf meinem Server in sich gekapselt läuft und dafür jeweils eigene Konfigurationsdateien und Umgebungsvariablen gemeinsam mit dem Service abgelegt bekommt. Ohne Hardlinks müsste ich den gesamten Service-Ordner versionieren und meine &lt;code&gt;.gitignore&lt;/code&gt; entsprechend komplex aufblasen.&lt;/p&gt;&#10;&lt;p&gt;Mit Git würde ich die per Hardlink abgebildeten Dateien versionieren und ihre Inhalte auf diese Weise auf Gitea verfügbar machen.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;code&gt;schallbert@server:/server-config-files&lt;/code&gt; &amp;ndash;&amp;gt; hardlinks &amp;ndash;&amp;gt; repository &amp;ndash;&amp;gt; Gitea&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Ich bekäme also eine nachgelagerte &amp;ldquo;Versionsbeobachtung&amp;rdquo; mit Sicherungskopie in dem Sinne, dass die Dateien nun mehrfach herumliegen.&lt;/p&gt;&#10;&lt;h3 id="2-repo-und-auto-deploy-auf-den-server"&gt;2. Repo und auto-deploy auf den Server&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-07-15-server-versioncontrol.jpg" alt="Image: Diagram how version control with auto-deploy might work on my server"&gt;&lt;/figure&gt;&#10;&lt;p&gt;In diesem Szenario halte ich die Konfigurationsdateien lokal auf meinem Laptop und kann sie wie üblich mit Git versionieren. Auf Gitea würde ich das Repository abbilden, und am Ende der Kette müsste mein Runner bei jeder Änderung der Konfiguration ein Auto-Deploy auf dem Server ausführen und die betroffenen Container anschließend neu starten.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;code&gt;schallbert@laptop:/server-config-files&lt;/code&gt; &amp;ndash;&amp;gt; repository &amp;ndash;&amp;gt; Gitea &amp;ndash;&amp;gt; act-runner &amp;ndash;&amp;gt; &lt;code&gt;server:/&amp;lt;service1...ServiceN&amp;gt;/config-files&lt;/code&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Ich bekäme in diesem Falle den Verwaltungs-&amp;ldquo;Main&amp;rdquo; auf meinen Laptop, und der Server würde der Abbildung auf Gitea folgen.&lt;/p&gt;&#10;&lt;h3 id="probleme"&gt;Probleme&lt;/h3&gt;&#10;&lt;p&gt;In beiden Fällen habe ich noch keine Möglichkeit, geänderte Konfigurationen vorab zu testen. Alles, was ich tue geht direkt nach &amp;ldquo;Prod&amp;rdquo; und wäre live. Schlimmstenfalls kann ich mir so recht leicht mein Setup zerschießen.&lt;/p&gt;&#10;&lt;p&gt;Ändert nun den &lt;em&gt;Status Quo ante&lt;/em&gt; nicht, bei dem ich die Dateien direkt auf dem Server verändere. Also ein neues Problem nur in der Hinsicht, dass ich beim Aufsetzen damals noch keinen Prod-Betrieb hatte und so kein Ausfallrisiko bestand.&lt;/p&gt;&#10;&lt;p&gt;Lösung Zwei scheint mir komplexer umzusetzen zu sein, denn ich benötige hierfür eine Deployment-Pipeline, welche die Dateien im Repository auf meinem Server ausrollt. Zumal sich der Runner in einem Docker-Container aufhält, während die Konfigurationsdateien direkt im Dateisystem des Servers befinden.&lt;/p&gt;&#10;&lt;p&gt;Ein direkter Durchgriff auf das Serverdateisystem &lt;a href="https://maze88.dev/docker-socket-from-within-containers.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ist zwar technisch möglich&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, würde aber meinem beschränkten Verständnis nach &lt;a href="https://dev.to/pbnj/docker-security-best-practices-45ih" target="_blank" rel="noopener noreferrer" class="external-link"&gt;eine große Angriffsfläche für alle meine Dienste&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bedeuten, deren Konfigurationsdateien nun über das Repository in Gitea direkt manipulierbar würden.&lt;/p&gt;&#10;&lt;h3 id="auswahl-meiner-lösung"&gt;Auswahl meiner Lösung&lt;/h3&gt;&#10;&lt;p&gt;Lösung Zwei lagert mir das Sofort-Live-Problem zumindest auf meinen Laptop vor, sodass ich in einem ersten Schritt nicht direkt am Prod-System herumfuhrwerken muss. Hier ließe sich, denke ich, leichter eine Integrationsumgebung aufsetzen, mit der ich meine Konfigurationsänderungen vorab prüfen kann. Da ich alle meine Dienste im Docker laufen habe, könnte sich das vielleicht ganz bequem lösen lassen.&lt;/p&gt;&#10;&lt;h2 id="frisch-ans-werk"&gt;Frisch ans Werk&lt;/h2&gt;&#10;&lt;h3 id="konfigurations-repo-anlegen"&gt;Konfigurations-Repo anlegen&lt;/h3&gt;&#10;&lt;p&gt;Gut, dann ziehe ich mir in einem ersten Schritt die Konfigurationsdateien vom Server. Dafür nutze ich das Dateitransferkommando &lt;a href="https://manpages.debian.org/bookworm/openssh-client/scp.1.en.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;scp&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;: &lt;code&gt;scp server:/path/to/source path/to/target&lt;/code&gt; etwa ein Dutzend mal, bis ich alle Konfigurationsdateien auf meinem Server erwischt habe.&lt;/p&gt;&#10;&lt;p&gt;Ich setze die Ordnerstruktur in diesem Repo direkt so auf, wie die Dateien auf dem Server liegen. So, hoffe ich, mache ich mir das Leben später etwas leichter.&lt;/p&gt;&#10;&lt;h3 id="secrets-in-docker-composeyml"&gt;Secrets in &lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Nur - was tue ich mit &amp;ldquo;secrets&amp;rdquo; in den Konfigurationsdateien? Also privaten Schlüsseln, registration tokens, hashes? Die möchte ich lieber nicht mehr oder minder offen im Repository herumliegen haben. Bei Verwendung von &lt;a href="https://docs.docker.com/compose/compose-file/05-services/#env_file" target="_blank" rel="noopener noreferrer" class="external-link"&gt;docker-compose&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist das das recht einfach: Ich kann geheime Werte in versteckte Dateien für Umgebungsvariablen auslagern und vom Git-Tracking ausnehmen. Im einfachsten Falle heißen solche Dateien schlicht &lt;code&gt;.env&lt;/code&gt; und beinhalten eine Aufzählung an Umgebungsvariablen im Stil von&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;BORG_PASSPHRASE&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;redacted&amp;gt;&amp;#34;&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In der entsprechenden &lt;code&gt;docker-compose.yml&lt;/code&gt; ziehe ich die Variable aus der Datei &lt;code&gt;.env&lt;/code&gt; wie folgt an:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- BORG_PASSPHRASE&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;BORG_PASSPHRASE&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diese Änderungen nehme ich direkt mal lokal auf meinem Laptop vor. Über die &lt;code&gt;.gitignore&lt;/code&gt; teile ich mittels &lt;code&gt;.*&lt;/code&gt; mit, dass versteckte Dateien und damit &lt;code&gt;.env&lt;/code&gt; nicht ins Repository aufgenommen werden sollen. Doch woher weiß ich nun, ob die Container noch korrekt hochfahren?&lt;/p&gt;&#10;&lt;h3 id="secrets-in-giteas-appini"&gt;Secrets in Giteas &lt;code&gt;app.ini&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Bei Gitea habe ich mich schon deutlich schwerer damit getan, Secrets in Dateien auszulagern. Die &lt;code&gt;app.ini&lt;/code&gt; wird auch von Gitea dynamisch geschrieben, sodass die Datei bei jedem Neustart des Services ein wenig anders aussieht. Nach langer Recherche fand ich&#10;&lt;a href="https://github.com/go-gitea/gitea/issues/25034" target="_blank" rel="noopener noreferrer" class="external-link"&gt;dieses Issue&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, in welchem eine Lösung für das separate Speichern von Secrets gesucht und gefunden wurde.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Sinnvoll erscheint mir nur das separate Speichern der Werte &lt;code&gt;INTERNAL_TOKEN&lt;/code&gt; sowie &lt;code&gt;SECRET_KEY&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Die anderen beiden &lt;a href="https://docs.gitea.com/next/administration/config-cheat-sheet/#server-server" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Properties&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; &lt;code&gt;LFS_JWT_SECRET&lt;/code&gt; und &lt;code&gt;JWT_SECRET&lt;/code&gt; werden ohnehin automatisch generiert und regelmäßig überschrieben.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;In meiner Konfiguration verwende ich lediglich &lt;code&gt;INTERNAL_TOKEN&lt;/code&gt;. Also werde jenes im Klartext und ohne Anführungszeichen in eine versteckte Datei kopieren (&lt;code&gt;.INTERNAL_TOKEN&lt;/code&gt;) und sie per Docker-Volume dem Container zur Verfügung stellen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Gitea&amp;#39;s docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./.INTERNAL_TOKEN:/run/secrets/INTERNAL_TOKEN:ro&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In der &lt;code&gt;app.ini&lt;/code&gt; ist es nun wichtig, den in der Composedatei angegebenen Pfad einzusetzen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Gitea&amp;#39;s app.ini&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;server&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;INTERNAL_TOKEN = /run/secrets/INTERNAL_TOKEN&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nicht etwa wie im oben verlinkten Issue angegeben &lt;code&gt;INTERNAL_TOKEN_URI=/run/secrets/INTERNAL_TOKEN&lt;/code&gt;, denn dies erzeugt in der von mir momentan verwendeten &lt;code&gt;V1.22.1&lt;/code&gt; einen Fehler: &lt;code&gt;Unsupported URI-Scheme&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="mal-ganz-grob-testen"&gt;Mal ganz grob testen&lt;/h3&gt;&#10;&lt;p&gt;Um zu prüfen, ob die geänderten Konfigurationsdateien noch funktionieren, installiere ich mir &lt;code&gt;docker&lt;/code&gt; und &lt;code&gt;docker-compose&lt;/code&gt; auf meinem Laptop. Anschließend probiere ich, die Container mal hochzufahren.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;# docker console log&#10;Error: Network &amp;#39;caddy-proxy&amp;#39; declared as external, but could not be found.&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Stimmt ja, Docker ist hier noch gar nicht konfiguriert. Also das Netzwerk erstellen: &lt;code&gt;sudo docker network create caddy-proxy&lt;/code&gt; und erneut versuchen. Schon beginnt der Download von Gitea samt Abhängigkeiten und der Container startet - wenn auch nicht so wie ich mir das vorgestellt habe: Innerhalb von Docker stimmen die Ordnerberechtigungen nicht, sodass weder Gitea, noch Act-runner auf alle benötigten Dateien zugreifen kann.&lt;/p&gt;&#10;&lt;p&gt;Dennoch, einen ersten Fehler finde ich:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;# gitea container log&#10;WARNING: The GITEA_RUNNER_REGISTRATION_TOKEN variable is not set. Defaulting to a blank string.&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ich hatte nämlich vergessen, den Token-String in Anführungszeichen zu setzen.&lt;/p&gt;&#10;&lt;p&gt;Für eine voll funktionsfähige Integrationsumgebung musste ich also noch mindestens zwei weitere Probleme lösen:&#10;Ordnerberechtigungen für den &lt;code&gt;Main&lt;/code&gt; auf meinem Laptop müssen so angelegt sein, dass auch der User &lt;code&gt;Docker&lt;/code&gt; Schreibrechte bekommt. Als einfache Lösung genügt vorerst, den betreffenden Volumes ein &lt;code&gt;:Z&lt;/code&gt; anzuhängen und sie so als &lt;a href="https://docs.docker.com/reference/cli/docker/container/run/#volumes-from" target="_blank" rel="noopener noreferrer" class="external-link"&gt;private unshared&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kennzuzeichnen. Eine Volumendefinition sieht in meiner &lt;code&gt;docker-compose.yml&lt;/code&gt; nun so aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./gitea:/data:Z&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich benötige eine zweite Datei für Umgebungsvariablen, um meine Dienste auf &lt;code&gt;localhost&lt;/code&gt; umzubiegen. Immerhin startet der Service auch so und ich sehe die Logausgabe. Die meisten Konfigurationsfehler kann ich bereits jetzt entdecken.&lt;/p&gt;&#10;&lt;p&gt;Ich bin mir nicht sicher, aber möglicherweise bekomme ich zusätzliche Probleme mit dem &lt;code&gt;caddyserver&lt;/code&gt; wie Zertifikatsverwaltung, Proxy-Einstellungen und so weiter.&lt;/p&gt;&#10;&lt;h2 id="eine-deploy-pipeline-erstellen"&gt;Eine Deploy-Pipeline erstellen&lt;/h2&gt;&#10;&lt;p&gt;Jetzt wäre es noch super, wenn die in das Gitea-Repo hochgeladenen (und vorab lokal auf Funktion getesteten) Dateien automatisch ihren Weg auf meinen Server finden würden. Hierfür könnte ich ein weiteres Docker-Volume anlegen, wo &lt;code&gt;act-runner&lt;/code&gt; dann die per &lt;code&gt;on:push&lt;/code&gt; Trigger abgelegten Daten hinpackt. Sie würden dann auf meinem Server verfügbar werden.&lt;/p&gt;&#10;&lt;h3 id="setup"&gt;Setup&lt;/h3&gt;&#10;&lt;p&gt;Erinnern wir uns an &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/#action-volumes"&gt;meine letzten Versuche&lt;/a&gt; mittels &lt;code&gt;act_runner&lt;/code&gt; Artefakte auf dem Server bereit zu stellen, so können wir einen Großteil dessen auch für diese Aufgabe übernehmen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# deploy-to-server.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Workflow for saving the server&amp;#39;s config repo to the local disk system&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Upload-server-config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;run-name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ gitea.actor }} uploads server configuration files&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;on&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;push&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;branches&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;main&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;jobs&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Deploy job&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;build&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt; &lt;span style="color:#75715e"&gt;# this is the &amp;#34;label&amp;#34; the runner will use and map to docker target OS&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container&lt;/span&gt;: &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;: &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# left: where the output will end up on disk, right: volume name inside container&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/opt/server-config:/workspace/schallbert/server-config/tmp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;CHECKOUT ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v3&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;: &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./tmp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;RUN FILE CHANGE TRIGGER ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; cd ./tmp/automation-hooks-trigger&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; touch server-config-update.txt&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="merkwürdige-volumefehler"&gt;Merkwürdige Volumefehler&lt;/h3&gt;&#10;&lt;p&gt;Doch der Weg bis hierhin war steinig. Lange hatte ich unter &lt;code&gt;volumes:&lt;/code&gt; auf der Containerseite lediglich &lt;code&gt;/server-config&lt;/code&gt; angegeben und nicht das Arbeitsverzeichnis des Runners. Dann läuft die Action zwar durch und alle Befehle der &lt;code&gt;#TEST&lt;/code&gt;-Sektion funktionieren auch. Wenn ich aber auf meinem Server nachschaue, bleibt der von Docker angelegte &lt;code&gt;server-config&lt;/code&gt; Ordner leer.&lt;/p&gt;&#10;&lt;p&gt;Mit folgenem Debug-Code unter &lt;code&gt;RUN FILE CHANGE TRIGGER&lt;/code&gt; helfe ich mir, die Pfadfehler zu finden:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# deploy-to-server.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#34;hello world&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; touch updated.txt&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; pwd&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ls -al&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mit &lt;code&gt;echo&lt;/code&gt; prüfe ich, ob mein Code im Runner überhaupt ausgeführt wird. Das &lt;code&gt;touch&lt;/code&gt;-Kommando setzt mir den aktuellen Zeitstempel in die &lt;code&gt;updated.txt&lt;/code&gt;-Datei, sodass ich dies später als &amp;ldquo;Hook&amp;rdquo; für weitere Automation verwenden kann. &lt;code&gt;pwd&lt;/code&gt; zeigt mir den aktiven Pfad innerhalb des Runners an, sodass ich das Docker-Volume korrekt auf die Server-Festplatte abbilden kann. &lt;code&gt;ls -al&lt;/code&gt; zeigt mir, ob die im Schritt &lt;code&gt;CHECKOUT&lt;/code&gt; zusammengestellten Konfigurationsdateien korrekt geschrieben wurden.&lt;/p&gt;&#10;&lt;p&gt;Ich erkenne dadurch, dass der Volume-Pfad auf der &amp;ldquo;rechten Seite&amp;rdquo; falsch war. Ich biege ihn entsprechend auf das aktive Verzeichnis des Runners um:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# deploy-to-server.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# left: where the output will end up on disk, right: volume name inside container&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/opt/server-config:/workspace/schallbert/server-config/&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Anschließend bekam ich Folgendes zu lesen:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;# gitea / act-runner console log&#10;failed to create container: &amp;#39;Error response from daemon: Duplicate mount point: /workspace/schallbert/server-config&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Anscheinend legt sich der Runner die &amp;ldquo;rechte Seite&amp;rdquo; des Mountpoints automatisch selbst an und kann daher nicht erneut belegt werden. Erst durch das Hinzufügen eines weiteren Pfadteiles, in meinem Falle &lt;code&gt;/tmp&lt;/code&gt; - siehe &lt;a href="https://blog.schallbert.de/server-config-version-control/#setup"&gt;oben&lt;/a&gt; - behebe ich den Fehler und auf meinem Server erscheint endlich 😌 der lang ersehnte Ordner &lt;code&gt;server-config&lt;/code&gt; mit Inhalt:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;schallbert@schallbert-ubuntu-:/opt/server-config# ls -al&#10;total 52&#10;drwxr-xr-x 9 root root 4096 Jul 12 15:38 .&#10;drwxr-xr-x 9 root root 4096 Jul 11 19:56 ..&#10;-rwxr-xr-x 1 root root 395 Jul 11 20:05 boot-after-backup.sh&#10;drwxr-xr-x 3 root root 4096 Jul 11 20:05 borgmatic&#10;drwxr-xr-x 2 root root 4096 Jul 11 20:05 caddy2&#10;drwxr-xr-x 3 root root 4096 Jul 11 20:05 fail2ban&#10;drwxr-xr-x 8 root root 4096 Jul 12 15:38 .git&#10;drwxr-xr-x 3 root root 4096 Jul 11 20:05 .gitea&#10;drwxr-xr-x 4 root root 4096 Jul 11 20:05 gitea&#10;-rw-r--r-- 1 root root 312 Jul 11 20:05 .gitignore&#10;-rw-r--r-- 1 root root 557 Jul 11 20:05 README.md&#10;-rwxr-xr-x 1 root root 371 Jul 11 20:05 shutdown-for-backup.sh&#10;-rw-r--r-- 1 root root 0 Jul 12 15:38 updated.txt&#10;drwxr-xr-x 2 root root 4096 Jul 11 20:05 watchtower&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="verteilen-der-konfiguration-auf-dem-server"&gt;Verteilen der Konfiguration auf dem Server&lt;/h2&gt;&#10;&lt;p&gt;Gut, damit sind die ersten Schritte getan. Ich habe nun ein vernünftig konfiguriertes Git-Repository, was meine Server-Konfiguration abbildet und vom Laptop aus gepflegt und zumindest rudimentär getestet werden kann. Außerdem kann ich über eine automatische &lt;code&gt;Action&lt;/code&gt; Updates der Konfiguration auf dem Server ablegen und eine Update-Datei mit Zeitstempel beschreiben.&lt;/p&gt;&#10;&lt;p&gt;Nun muss das Update auf dem Server noch entgegengenommen, verteilt und die betroffenen Programme und Dienste sollen neu gestartet werden. Doch dies schauen wir uns im Artikel &lt;a href="https://blog.schallbert.de/server-config-deploy/"&gt;Serverkonfiguration ausrollen&lt;/a&gt; an.&lt;/p&gt;&#10;</description></item><item><title>'redir' im Caddyserver</title><link>https://blog.schallbert.de/migrating-to-subdomain/</link><pubDate>Tue, 30 Jan 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/migrating-to-subdomain/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-01-30-migrating-to-subdomain-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumbnail of correctly rendered &amp;#39;&amp;#39;website under construction&amp;#39;&amp;#39; page on schallbert.de"&#10; title="&amp;#39;redir&amp;#39; im Caddyserver" /&gt;&#10;&lt;p&gt;Als ich meine Website weg von Github Pages auf einen eigenen Server &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/"&gt;umzog&lt;/a&gt;, hatte ich einige Probleme und offene Fragen bezüglich automatischer Weiterleitungen. Mein Blog ging nämlich von der root-Domain in eine Subdomain (blog.schallbert.de) über.&lt;/p&gt;&#10;&lt;h2 id="anforderungen-an-die-weiterleitung"&gt;Anforderungen an die Weiterleitung&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Bereits auf meine Blogartikel unter altem Namen referenzierte Links z.B. auf &lt;a href="https://blog.schallbert.de/about/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;schallbert.de/about&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; sollen korrekt auf die Subdomain weiterleiten (kein &lt;em&gt;404&lt;/em&gt;)&lt;/li&gt;&#10;&lt;li&gt;Die Stammseite unter &lt;code&gt;schallbert.de&lt;/code&gt; soll vollständig nutzbar sein&lt;/li&gt;&#10;&lt;li&gt;Daten und Dateien für Domain und Subdomains sollen unabhängig voneinander sein&lt;/li&gt;&#10;&lt;li&gt;Die Webserver sollen für verschiedene Unterseiten unterschiedliche Technologien verwenden können&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="umsetzung"&gt;Umsetzung&lt;/h2&gt;&#10;&lt;p&gt;Zuerst hatte ich gedacht, dass ich die Weiterleitungen im DNS-provider über &lt;code&gt;A&lt;/code&gt; und &lt;code&gt;CNAME&lt;/code&gt; Einträge erreichen könnte. Schnell wurde mir jedoch klar, dass ich &lt;em&gt;a)&lt;/em&gt; keine Ahnung hatte, wie DNS überhaupt funktioniert und &lt;em&gt;b)&lt;/em&gt; der richtige Weg über die Konfiguration meines Webservers führt.&lt;/p&gt;&#10;&lt;h3 id="erster-versuch-redir-bei-"&gt;Erster Versuch: redir bei &amp;lsquo;/&amp;rsquo;&lt;/h3&gt;&#10;&lt;p&gt;Auf meinem Webserver (Caddy) werden Weiterleitungen in der &lt;code&gt;Caddyfile&lt;/code&gt; per &lt;code&gt;redir&lt;/code&gt; spezifiziert. Dabei kann das &lt;code&gt;permanent&lt;/code&gt; (als sogenannte http &lt;code&gt;301&lt;/code&gt;) oder &lt;code&gt;temporary&lt;/code&gt; per &lt;code&gt;302&lt;/code&gt; (Standardeinstellung) geschehen.&lt;/p&gt;&#10;&lt;p&gt;Mein erster Gedanke war, dass ich &lt;code&gt;schallbert.de&lt;/code&gt; unmodifiziert lasse und alles hinter einem eventuellen &lt;code&gt;/&lt;/code&gt; weiterleite. Dies sieht dann in der Caddyfile wie folgt aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;schallbert.de {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Define webserver&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;root * /www/landing&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;encode gzip&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;file_server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Redir to subdomain if domain has a slash&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@redirect path_regexp /*&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;redir @redirect https://blog.schallbert.de{uri}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In der Folge hat Caddy schlicht alles an die Subdomain weitergeleitet. Der Haken dabei war, dass die Subdomain selbst ja auch wieder Slashes bzw. eine URI hinter dem Slash enthält und dann wiederum weitergeleitet wird. Beispiel:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;schallbert.de/post -&amp;gt; blog.schallbert.de/post -&amp;gt; blog.schallbert.de/post -&amp;gt; blog.schallbert.de/post -&amp;gt; [...]&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Somit bekam ich eine rekursive Endlosweiterleitung. Ich habe meinen Server damit komplett lahmgelegt 😅&lt;/p&gt;&#10;&lt;h3 id="zweiter-versuch-filterung-per-regexp"&gt;Zweiter Versuch: Filterung per Regexp&lt;/h3&gt;&#10;&lt;p&gt;Ich brauche also eine bessere Filterung, sodass nur Slashes mit weiteren Zeichen dahinter auch wirklich weitergeleitet werden und die Weiterleitung nicht rekursiv ablaufen kann. Zum Glück hatte ich Hilfe von einem guten Freund, sodass kurze Zeit darauf folgendes &lt;code&gt;Caddyfile&lt;/code&gt; entstand:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;schallbert.de {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Define webserver&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;root * /www/landing&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;encode gzip&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;file_server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Redir to subdomain to maintain links for blog&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@redirect path_regexp ^/[^/]+(/.*)?$&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;redir @redirect https://blog.schallbert.de{uri}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die Regexp prüft ab, ob ein Slash hinter der Domain URL vorliegt und dahinter zumindest ein weiteres Zeichen folgt. Hier funktioniert die Weiterleitung nun wie gewünscht und mein Blog ist auch unter den alten Links weiterhin vollständig erreichbar.&lt;/p&gt;&#10;&lt;p&gt;Trotzdem war ich noch nicht ganz zufrieden. Denn als ich mir eine schöne &amp;ldquo;Landing Page&amp;rdquo; auf meiner Root Domain erstellte, war von ihr plötzlich nur noch das reine HTML zu sehen. Kein favicon, keine Bilder, kein CSS.&lt;/p&gt;&#10;&lt;p&gt;Ein kurzer Blick in die Entwickleroptionen brachte schnell die Erkenntnis dass die Pfade aller Assets, die ich auf meiner Landing Page hinterlegt hatte, durch die Ordnerstruktur mit &lt;code&gt;/&lt;/code&gt; gekennzeichnet, an die Subdomain weitergeleitet wurden. Wo sie natürlich nicht liegen.&lt;/p&gt;&#10;&lt;h3 id="dritter-versuch-regexp-plus-dateisuche"&gt;Dritter Versuch: Regexp plus Dateisuche&lt;/h3&gt;&#10;&lt;p&gt;Zum Glück bin ich wieder mal mit dem Problem nicht allein, sodass im &lt;a href="https://caddy.community/t/redirect-if-file-not-present/7902" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Forum von Caddy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; die Lösung zu finden war:&lt;/p&gt;&#10;&lt;p&gt;Mit dem &lt;code&gt;not file&lt;/code&gt; Marker kann ich in der &lt;code&gt;Caddyfile&lt;/code&gt; Weiterleitungen definieren, falls Ressourcen auf der aktuellen Seite unauffindbar sind.&lt;/p&gt;&#10;&lt;p&gt;Somit sieht meine Lösung jetzt wie folgt aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;schallbert.de {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@filenotfound {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;path_regexp ^/[^/]+(/.*)?$&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;not file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# show Caddy where to find page resources&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;root * /www/landing&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;encode gzip&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;route {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Redirect to subdomain if article cannot found on root&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;redir @filenotfound https://blog.schallbert.de{uri}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# else, define webserver and show page&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;file_server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hier ist wichtig, dass ich die Seite mit &lt;code&gt;root * /www/&amp;lt;pagelocation&amp;gt;&lt;/code&gt; definiere, bevor ich die &lt;code&gt;route&lt;/code&gt; anlege. Denn sonst werden die Assets erst verfügbar, nachdem ich bereits aus Ermangelung vorhandener Dateien weitergeleitet habe.&lt;/p&gt;&#10;&lt;h2 id="erfolg"&gt;Erfolg!&lt;/h2&gt;&#10;&lt;p&gt;Jetzt endlich wird meine Baustellenseite korrekt dargestellt.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-01-30-migrating-to-subdomain.jpg" alt="Image: Correctly rendered &amp;#39;website under construction&amp;#39; page on schallbert.de"&gt;&lt;/figure&gt;&#10;</description></item><item><title>Automatische Updates für alles!</title><link>https://blog.schallbert.de/server-auto-upgrade/</link><pubDate>Sat, 20 Jan 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/server-auto-upgrade/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-01-20-server-auto-upgrade-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: unattended-upgrades and Watchtower keep my server and application packages up to date."&#10; title="Automatische Updates für alles!" /&gt;&#10;&lt;p&gt;Wie der Titel schon sagt, möchte ich Sicherheitsupdates gern installiert haben, sobald sie erscheinen. Dies gilt gleichsam für das Ubuntu meines Cloud-Servers wie auch für alle Applikationen, die ich in Docker betreibe. Da ich faul bin, möchte ich diese Sicherheitsupdates nicht manuell vornehmen, sondern vollautomatisch ablaufen lassen.&lt;/p&gt;&#10;&lt;p&gt;Für Funktionsupdates und -Erweiterungen hingegen ist es auch weiterhin OK für mich, von Zeit zu Zeit manuell einzugreifen.&#10;Beginnen wir also mit den Systemupgrades.&lt;/p&gt;&#10;&lt;h2 id="sicherheitsupdates-per-unattended-upgrades"&gt;Sicherheitsupdates per &lt;code&gt;unattended-upgrades&lt;/code&gt;&lt;/h2&gt;&#10;&lt;p&gt;Ubuntu stellt - wie eine kurze &lt;a href="https://askubuntu.com/questions/9/how-do-i-enable-automatic-updates" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Suchmaschinenabfrage ergibt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; - bereits ein konfigurierbares Werkzeug für solche automatisierten Updates zur Verfügung: &lt;code&gt;unattended-upgrades&lt;/code&gt;. Ich prüfe zuerst, ob es bereits auf meinem System vorhanden ist:&lt;/p&gt;&#10;&lt;h3 id="installation"&gt;Installation&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert:~# which unattended-upgrades&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/unattended-upgrades&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Prima, es ist bereits da. Ansonsten hätte ich die Installation schnell mit &lt;code&gt;apt install unattended-upgrades&lt;/code&gt; nachgeholt.&lt;/p&gt;&#10;&lt;h3 id="unattended-upgrades-konfigurieren"&gt;unattended-upgrades konfigurieren&lt;/h3&gt;&#10;&lt;p&gt;Hier hilft das &lt;a href="https://wiki.debian.org/UnattendedUpgrades" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Debian Wiki&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; weiter: Die Konfigurationsdateien liegen in &lt;code&gt;/etc/apt/apt.conf.d&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Zuerst gehe ich sicher, dass automatische Updates aktiviert sind. Hierfür prüfe ich die Datei &lt;code&gt;20auto-upgrades&lt;/code&gt; und stelle sicher, dass Paketindex-Updates und Upgradeinstallation aktiviert sind:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;APT::Periodic::Update-Package-Lists &lt;span style="color:#e6db74"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;APT::Periodic::Unattended-Upgrade &lt;span style="color:#e6db74"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Sehr gut. Jetzt sollte ich noch festlegen, zu welchem Zeitpunkt das Tool loslaufen und welcher Typ Updates installiert werden soll. Hierfür öffne ich die Datei &lt;code&gt;50unattended-upgrades&lt;/code&gt; und entferne die Kommentare bei den entsprechenden Zeilen.&lt;/p&gt;&#10;&lt;h3 id="updatequellen-festlegen"&gt;Updatequellen festlegen&lt;/h3&gt;&#10;&lt;p&gt;Der erste Abschnitt der Konfigurationsdatei widmet sich der erlaubten Quellen für die Updates. Hier sollten selbstverständlich nur vertrauenswürdige Provider stehen. Meine Konfig sieht hier wie folgt aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unattended-Upgrade::Allowed-Origins &lt;span style="color:#f92672"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_codename&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_codename&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;-security&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;ESMApps:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_codename&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;-apps-security&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;ESM:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_codename&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;-infra-security&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Erlaubt werden hier Quellen für die von mir verwendete Linux-Distribution im Allgemeinen und Sicherheitsupdates inklusive &lt;code&gt;ESM&lt;/code&gt;, was für &amp;ldquo;Extended Security Maintenance&amp;rdquo; steht.&lt;/p&gt;&#10;&lt;h3 id="updates-auch-für-dev-releases"&gt;Updates auch für Dev Releases?&lt;/h3&gt;&#10;&lt;p&gt;Die folgende Zeile legt fest, ob auch Vorabversionen von Ubuntu Updates erhalten sollen. Bei mir steht der Wert auf &lt;code&gt;auto&lt;/code&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unattended-Upgrade::DevRelease &lt;span style="color:#e6db74"&gt;&amp;#34;auto&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="automatischer-neustart"&gt;Automatischer Neustart&lt;/h3&gt;&#10;&lt;p&gt;Einige Sicherheitsupdates (z.B. den &lt;a href="https://en.wikipedia.org/wiki/Kernel_%28operating_system%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Kernel&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; betreffend) erfordern einen Neustart der Maschine. Sie werden daher nur in Kraft treten, wenn man in der Datei entsprechende Werte gesetzt hat. Dabei ist &lt;code&gt;WithUsers&lt;/code&gt; natürlich Geschmackssache, denn man wird ausgeloggt, falls zufälligerweise gerade ein Update installiert wurde, was einen Neustart erfordert. Um das praktisch auszuschließen, habe ich unter &lt;code&gt;Reboot-Time&lt;/code&gt; eine Zeit eingetragen, bei der ich sicher nicht an der Maschine hocke.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unattended-Upgrade::Automatic-Reboot &lt;span style="color:#e6db74"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unattended-Upgrade::Automatic-Reboot-WithUsers &lt;span style="color:#e6db74"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unattended-Upgrade::Automatic-Reboot-Time &lt;span style="color:#e6db74"&gt;&amp;#34;03:45&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Unangekündigte Neustarts haben natürlich Rückwirkung auf alle Programme, die ich verwende. Daher stelle ich in meinen &lt;code&gt;docker-compose.yml&lt;/code&gt; Dateien sicher, dass dort bei &lt;code&gt;restart:&lt;/code&gt; auch wirklich &lt;code&gt;always&lt;/code&gt; oder &lt;code&gt;unless-stopped&lt;/code&gt; notiert ist.&lt;/p&gt;&#10;&lt;h3 id="benachrichtigungen"&gt;Benachrichtigungen&lt;/h3&gt;&#10;&lt;p&gt;Automatische Mails oder anderweitige Push-Benachrichtigungen habe ich erst einmal nicht eingeschaltet, da ich mich noch im Testbetrieb befinde und keine große Lust auf einen Haufen weiterer Nachrichten habe. Sobald ich dies ändere, gibt es hier ganz bestimmt ein Update.&lt;/p&gt;&#10;&lt;h3 id="funktionsprüfung"&gt;Funktionsprüfung&lt;/h3&gt;&#10;&lt;p&gt;Um zu sehen, ob unattended-upgrades tut was es soll, schaue ich ein paar Tage später mal in die Logs unter &lt;code&gt;/var/log/unattended-upgrades&lt;/code&gt;.&#10;Hier steht in der Datei &lt;code&gt;unattended-upgrades.log&lt;/code&gt; zum Beispiel folgendes:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;59&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;781&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Starting&lt;/span&gt; unattended upgrades script&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;59&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;781&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Allowed&lt;/span&gt; origins &lt;span style="color:#e6db74"&gt;are&lt;/span&gt;: o&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;Ubuntu&lt;/span&gt;,a&lt;span style="color:#f92672"&gt;=&lt;/span&gt;jammy, o&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;Ubuntu&lt;/span&gt;,a&lt;span style="color:#f92672"&gt;=&lt;/span&gt;jammy&lt;span style="color:#f92672"&gt;-&lt;/span&gt;security, o&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;UbuntuESMApps&lt;/span&gt;,a&lt;span style="color:#f92672"&gt;=&lt;/span&gt;jammy&lt;span style="color:#f92672"&gt;-&lt;/span&gt;apps&lt;span style="color:#f92672"&gt;-&lt;/span&gt;security, o&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;59&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;781&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Initial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;blacklist&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;59&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;782&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Initial&lt;/span&gt; whitelist (&lt;span style="color:#f92672"&gt;not&lt;/span&gt; strict):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;33&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;956&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Packages&lt;/span&gt; that will be &lt;span style="color:#e6db74"&gt;upgraded&lt;/span&gt;: libc&lt;span style="color:#f92672"&gt;-&lt;/span&gt;bin libc&lt;span style="color:#f92672"&gt;-&lt;/span&gt;dev&lt;span style="color:#f92672"&gt;-&lt;/span&gt;bin libc&lt;span style="color:#f92672"&gt;-&lt;/span&gt;devtools libc6 libc6&lt;span style="color:#f92672"&gt;-&lt;/span&gt;dev locales python3&lt;span style="color:#f92672"&gt;-&lt;/span&gt;twisted&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;33&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;956&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Writing&lt;/span&gt; dpkg log to &lt;span style="color:#e6db74"&gt;/var/&lt;/span&gt;log&lt;span style="color:#f92672"&gt;/&lt;/span&gt;unattended&lt;span style="color:#f92672"&gt;-&lt;/span&gt;upgrades&lt;span style="color:#f92672"&gt;/&lt;/span&gt;unattended&lt;span style="color:#f92672"&gt;-&lt;/span&gt;upgrades&lt;span style="color:#f92672"&gt;-&lt;/span&gt;dpkg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;54&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;578&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;All&lt;/span&gt; upgrades installed&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die Datei &lt;code&gt;unattended-upgrades-shutdown.log&lt;/code&gt; ist allerdings noch leer. Ich werde zu einem späteren Zeitpunkt prüfen, ob hier auch alles funktioniert.&lt;/p&gt;&#10;&lt;h2 id="container-updates-mit-watchtower-automatisieren"&gt;Container-Updates mit &lt;code&gt;Watchtower&lt;/code&gt; automatisieren&lt;/h2&gt;&#10;&lt;p&gt;Nun zu den Updates meiner Applikationen. Ein kurzes Gespräch mit ein paar Admins ergab:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Es ist mühsam, alle Programme von Hand auf Stand zu halten&lt;/li&gt;&#10;&lt;li&gt;Für Anwendungen im Container gibt es Dienste, die diese Aufgabe zentral lösen&lt;/li&gt;&#10;&lt;li&gt;Es gab bereits schlechte Erfahrungen, wenn in der &lt;code&gt;docker-compose.yml&lt;/code&gt; auf den aktuellsten Release referenziert wird&lt;/li&gt;&#10;&lt;li&gt;Bei &lt;code&gt;image: &amp;lt;application&amp;gt;:latest&lt;/code&gt; seien hier nicht alle Anwendungen stabil bzw. erzeugten Probleme mit Abhängigkeiten&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Einer Lösung für dieses Problem bietet &lt;a href="https://containrrr.dev/watchtower/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Watchtower&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, welches in meinem Falle selbst wieder in einem Container läuft.&lt;/p&gt;&#10;&lt;h3 id="installation-1"&gt;Installation&lt;/h3&gt;&#10;&lt;p&gt;Wie üblich schreibe ich mir in einem neuen Ordner unter &lt;code&gt;opt/watchtower&lt;/code&gt; eine &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# copied from https://containrrr.dev/watchtower/notifications/&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# watchtower/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;3&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;services&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;watchtower&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;containrrr/watchtower&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;restart&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;unless-stopped&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Watchtower benötigt &lt;code&gt;docker.sock&lt;/code&gt;, um die Updates zu ziehen und auf den vorhandenen Containern anzuwenden.&#10;Ansonsten habe ich hier erst einmal nichts weiter unternommen. Keine Benachrichtigungen (aus obengenannten Gründen), keine Logs, nichts. Ich hoffe, dieses Setup bleibt geräuschlos und ich muss mich hier lange nicht mit einem &lt;em&gt;Update&lt;/em&gt; zu Wort melden.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: (haha) neue Container-Quelle für watchtower&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-02-20T00:00:00Z"&gt;&#10; 20. Feb. 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Die Watchtower-Lösung von &lt;em&gt;containrrr&lt;/em&gt; funktioniert auf meinem System nicht mehr. Sie scheint nicht mehr gewartet zu werden. Also wechsele ich in der &lt;code&gt;docker-compose.yml&lt;/code&gt; zu: &lt;code&gt;image: nickfedor/watchtower:latest&lt;/code&gt;. Langfristig aber ist die Übergabe des docker sock an einen Container &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-1-do-not-expose-the-docker-daemon-socket-even-to-the-containers" target="_blank" rel="noopener noreferrer" class="external-link"&gt;gar keine gute Idee&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Auf meinem Zettel steht bereits, dafür eine weniger vulnerable Lösung per Skript zu finden.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: Watchtower durch Skript abgelöst&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-11-01T00:00:00Z"&gt;&#10; 01. Nov. 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Ich habe automatische Image-Updates meiner Container durch ein Skript eingeführt. Mein Vorgehen beschreibe ich im Artikel &lt;a href="https://blog.schallbert.de/auto-upgrade-docker-images/"&gt;auto-upgrade docker images&lt;/a&gt;&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;p&gt;Weitergehende Schritte für die Validierung der Funktion von Watchtower finden sich z.B. &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-automatically-update-docker-container-images-with-watchtower-on-ubuntu-22-04" target="_blank" rel="noopener noreferrer" class="external-link"&gt;in einem Tutorial von DigitalOcean&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="ablauf"&gt;Ablauf&lt;/h3&gt;&#10;&lt;p&gt;Watchtower prüft die Installierten Docker images &lt;a href="https://containrrr.dev/watchtower/introduction/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;lt. Doku täglich&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; auf Updates. Sollte des ein neues Image-release geben, schickt Watchtower ein &lt;code&gt;SIGTERM&lt;/code&gt;-Signal an upzudatende Container, worauf sich selbige herunterfahren (&lt;a href="https://containrrr.dev/watchtower/stop-signals/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Quelle&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;). Anschließend werden die Container wieder hochgefahren.&lt;/p&gt;&#10;&lt;h3 id="unfreiwillige-funktionsprüfung"&gt;Unfreiwillige Funktionsprüfung&lt;/h3&gt;&#10;&lt;p&gt;Ein paar Tage nach Start des Containers wählte ich mich nochmal in meinen Server ein und sah auf gut Glück ein paar Logs durch. Ich wollte nur kurz nach dem Rechten schauen.&lt;/p&gt;&#10;&lt;p&gt;In den &lt;code&gt;fail2ban&lt;/code&gt; Logdateien sah ich zu einem bestimmten Zeitpunkt, Uhrzeit &lt;code&gt;14:42:56&lt;/code&gt; plötzlich seitenweise Einträge. Mehr davon, als ich die Tage vorher insgesamt gesehen hatte. Als ich zum Beginn dieser Kette von Einträgen hochscrollte, sah ich, dass fail2ban mir wohl kurz ausgestiegen war.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt; 2024-01-26 14:42:50,817 &amp;lt;container_id&amp;gt; INFO Shutdown in progress...&#10; 2024-01-26 14:42:50,817 &amp;lt;container_id&amp;gt; INFO Observer stop ... try to end queue 5 seconds&#10; 2024-01-26 14:42:50,837 &amp;lt;daemon_id&amp;gt; INFO Observer stopped, 0 events remaining.&#10; 2024-01-26 14:42:50,878 &amp;lt;container_id&amp;gt; INFO Stopping all jails&#10; 2024-01-26 14:42:50,879 &amp;lt;container_id&amp;gt; INFO Removed logfile: &amp;#39;/var/log/auth.log&amp;#39;&#10; 2024-01-26 14:42:50,879 &amp;lt;container_id&amp;gt; INFO Removed logfile: &amp;#39;/var/log/caddy2/gitea_access.log&amp;#39;&#10; 2024-01-26 14:42:50,879 &amp;lt;container_id&amp;gt; INFO Removed logfile: &amp;#39;/var/log/caddy2/server_access.log&amp;#39;&#10; 2024-01-26 14:42:51,052 &amp;lt;daemon_id&amp;gt; NOTIC [sshd] Flush ticket(s) with iptables&#10; 2024-01-26 14:42:51,063 &amp;lt;container_id&amp;gt; INFO Jail &amp;#39;sshd&amp;#39; stopped&#10; 2024-01-26 14:42:51,137 &amp;lt;daemon_id&amp;gt; NOTIC [caddy-status] Flush ticket(s) with iptables-multiport&#10; 2024-01-26 14:42:51,137 &amp;lt;container_id&amp;gt; INFO Jail &amp;#39;caddy-status&amp;#39; stopped&#10; 2024-01-26 14:42:51,138 &amp;lt;container_id&amp;gt; INFO Connection to database closed.&#10; 2024-01-26 14:42:51,139 &amp;lt;container_id&amp;gt; INFO Exiting Fail2ban&#10; 2024-01-26 14:42:56,173 &amp;lt;new_container_id&amp;gt; INFO --------------------------------------------------&#10; 2024-01-26 14:42:56,173 &amp;lt;new_container_id&amp;gt; INFO Starting Fail2ban v1.0.2&#10; 2024-01-26 14:42:56,173 &amp;lt;new_container_id&amp;gt; INFO Observer start...&#10;[...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Die ganzen Log-Einträge zur selben Uhrzeit beginnen am Ende des oben gezeigten Abschnitts. Ein ganzer Haufen IP-Adressen wird dort zur Vorbereitung des Banns in Fail2ban geladen.&#10;Leicht beunruhigt schaute ich ins Syslog von Ubuntu:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;Jan 26 14:42:55 schallbert systemd[1]: docker-&amp;lt;ID&amp;gt;.scope: Deactivated successful&#10;Jan 26 14:42:55 schallbert systemd[1]: docker-&amp;lt;ID&amp;gt;.scope: Consumed 21:05 CPU time&#10;Jan 26 14:42:55 schallbert dockerd[690]: time=&amp;#34;2024-01-26T14:42:55.228607617Z&amp;#34; level=info msg=&amp;#34;ignoring event&amp;#34; &#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.229864632Z&amp;#34; level=info msg=&amp;#34;shim disconnected&amp;#34; &#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.230125409Z&amp;#34; level=warning msg=&amp;#34;cleaning up after shim disconnected&amp;#34; i&amp;gt;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.230145478Z&amp;#34; level=info msg=&amp;#34;cleaning up dead shim&amp;#34;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.240586557Z&amp;#34; level=warning msg=&amp;#34;cleanup warnings time=\&amp;#34;2024-01-26T14:&amp;gt;&#10;Jan 26 14:42:55 schallbert dockerd[690]: time=&amp;#34;2024-01-26T14:42:55.241839655Z&amp;#34; level=warning msg=&amp;#34;ShouldRestart failed&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.870912387Z&amp;#34; level=info msg=&amp;#34;loading plugin \&amp;#34;io.containerd.event.v1.p&amp;gt;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.870997689Z&amp;#34; level=info msg=&amp;#34;loading plugin \&amp;#34;io.containerd.internal.v&amp;gt;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.871007368Z&amp;#34; level=info msg=&amp;#34;loading plugin \&amp;#34;io.containerd.ttrpc.v1.t&amp;gt;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.871150831Z&amp;#34; level=info msg=&amp;#34;starting signal loop&amp;#34; namespace=moby path&amp;gt;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hm, &lt;code&gt;cleaning up dead shim&lt;/code&gt;. Das klingt ja bedrohlich. Eine kurze &lt;a href="https://iximiuz.com/en/posts/implementing-container-runtime-shim/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Recherche&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zeigt, dass die Shim eine Zwischenschicht zwischen Containermanager und dem Container selbst bildet und Ein-und Ausgaben des Containers weiterleitet. Ihre Laufzeit ist an die des Containers gekoppelt.&lt;/p&gt;&#10;&lt;p&gt;Also war dies ein normales Herunterfahren mit anschließendem Neustart. Zum Glück kein Einbruch!&#10;Bleibt nur noch der Grund für den Neustart von Fail2ban herauszufinden. Da fällt mir ein, dass ich noch gar nicht weiß, wann Watchtower genau die Updates fährt&amp;hellip;&lt;/p&gt;&#10;&lt;p&gt;Also mal schnell in die Logs von Watchtower geschaut mit &lt;code&gt;docker container logs &amp;lt;container_id&amp;gt;&lt;/code&gt; und siehe da:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;time=&amp;#34;2024-01-26T14:42:47Z&amp;#34; level=info msg=&amp;#34;Found new lscr.io/linuxserver/fail2ban:latest image (9bda077d765e)&amp;#34;&#10;time=&amp;#34;2024-01-26T14:42:50Z&amp;#34; level=info msg=&amp;#34;Stopping /fail2ban (e0317a105d53) with SIGTERM&amp;#34;&#10;time=&amp;#34;2024-01-26T14:42:55Z&amp;#34; level=info msg=&amp;#34;Creating /fail2ban&amp;#34;&#10;time=&amp;#34;2024-01-26T14:42:55Z&amp;#34; level=info msg=&amp;#34;Session done&amp;#34; Failed=0 Scanned=6 Updated=1 notify=no&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Damit habe ich den Beweis: Watchtower tut was es soll. Es zieht Container auf eine neue Version hoch, sobald Updates verfügbar werden. Prima!&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;PS:&lt;/strong&gt; Da ich Watchtower nicht groß konfiguriert habe, nehme ich anhand der in den Logs befindlichen Zeitstempel an, dass die Updates täglich zur Zeit des Starts von Watchtower gefahren werden. Bald werde ich von &lt;code&gt;latest&lt;/code&gt; für meine Container wohl auf ein älteres Tag release umschwenken.&lt;/p&gt;&#10;</description></item><item><title>Den Server absichern</title><link>https://blog.schallbert.de/server-protection/</link><pubDate>Fri, 12 Jan 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/server-protection/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-01-12-server-protection-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: side-by-side image of fail2ban and borgmatic logos"&#10; title="Den Server absichern" /&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;OK. Nun läuft mein Server, &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/"&gt;baut automatisch&lt;/a&gt; meinen Blog und &lt;a href="https://blog.schallbert.de/self-hosted-jekyll-page-broken-links/"&gt;zeigt ihn auch problemlos an&lt;/a&gt;. So ganz fertig bin ich aber noch nicht. Denn ich könnte mehr für die Sicherheit des Servers tun, als überall Passwort-Logins zu deaktivieren.&lt;/p&gt;&#10;&lt;p&gt;Außerdem habe ich noch keine Backups. Und das ist ja nie gut. Also, packen wir&amp;rsquo;s an!&lt;/p&gt;&#10;&lt;h2 id="unerwünschte-besucher-aussperren"&gt;Unerwünschte Besucher aussperren&lt;/h2&gt;&#10;&lt;p&gt;Ich bekomme in meinem SSH-Log im Root-Account des Servers einen Haufen Verbindungsversuche, die nicht von mir kommen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;29&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Invalid&lt;/span&gt; user admin from &lt;span style="color:#ae81ff"&gt;41&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;207&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;248&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;204&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;37194&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;29&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: pam_unix(&lt;span style="color:#e6db74"&gt;sshd&lt;/span&gt;:auth): check pass; user unknown&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;29&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: pam_unix(&lt;span style="color:#e6db74"&gt;sshd&lt;/span&gt;:auth): authentication failure; logname&lt;span style="color:#f92672"&gt;=&lt;/span&gt; uid&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; euid&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; tty&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ssh ru&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;31&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Failed&lt;/span&gt; password &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; invalid user admin from &lt;span style="color:#ae81ff"&gt;41&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;207&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;248&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;204&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;37194&lt;/span&gt; ssh2&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Connection&lt;/span&gt; closed by invalid user admin &lt;span style="color:#ae81ff"&gt;41&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;207&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;248&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;204&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;37194&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;preauth&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Invalid&lt;/span&gt; user svn from &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;108&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;44968&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: pam_unix(&lt;span style="color:#e6db74"&gt;sshd&lt;/span&gt;:auth): check pass; user unknown&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: pam_unix(&lt;span style="color:#e6db74"&gt;sshd&lt;/span&gt;:auth): authentication failure; logname&lt;span style="color:#f92672"&gt;=&lt;/span&gt; uid&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; euid&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; tty&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ssh ru&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Failed&lt;/span&gt; password &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; invalid user svn from &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;108&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;44968&lt;/span&gt; ssh2&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Received&lt;/span&gt; disconnect from &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;108&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;44968&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Bye&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Bye&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;preauth&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Disconnected&lt;/span&gt; from invalid user svn &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;108&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;44968&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;preauth&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wenn ich nicht wüsste, dass dies das inzwischen übliche &amp;ldquo;Rauschen&amp;rdquo; im Internet ist, würde es mich schon etwas beunruhigen. Ist ja fast so, als würde alle paar Sekunden jemand mit bösen Absichten versuchen, irgendeinen Schlüssel an meiner Haustür auszuprobieren. Was kann man dagegen also tun? Wegschicken!&lt;/p&gt;&#10;&lt;h3 id="fail2ban"&gt;fail2ban&lt;/h3&gt;&#10;&lt;p&gt;Genau dies soll die Software &lt;a href="https://github.com/fail2ban/fail2ban" target="_blank" rel="noopener noreferrer" class="external-link"&gt;fail2ban&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für mich übernehmen.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; __ _ _ ___ _ &#10; / _|__ _(_) |_ ) |__ __ _ _ _ &#10; | _/ _` | | |/ /| &amp;#39;_ \/ _` | &amp;#39; \ &#10; |_| \__,_|_|_/___|_.__/\__,_|_||_|&#10; v1.1.0.dev1 20??/??/??&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Vereinfacht gesagt durchforstet &lt;code&gt;fail2ban&lt;/code&gt; Zugriffslogs&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; nach IP-Adressen, für die fehlgeschlagene Anmeldeversuche registriert wurden, und &amp;ldquo;bannt&amp;rdquo; sie bei Überschreiten einer benutzerdefinierten Anzahl Versuche innerhalb einer bestimmten Zeit für einen gewünschten Zeitraum.&lt;/p&gt;&#10;&lt;p&gt;Wie dieser Bannspruch umgesetzt wird? Fail2ban modifiziert die &lt;a href="https://en.wikipedia.org/wiki/Iptables" target="_blank" rel="noopener noreferrer" class="external-link"&gt;iptables&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, greift also auf Paketfilterregeln zu (Stichwort Firewall), welche sich unten auf der Netzwerkschicht befinden. So werden hereinkommende Anfragen bereits geblockter Adressen gar nicht erst bis zu meinen Applikationen durchkommen&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#10;&lt;h3 id="fail2ban-installieren"&gt;fail2ban installieren&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;fail2ban&lt;/code&gt; scheint so etwas wie Industriestandard bei der Abwehr unerwünschter Zugriffsversuche auf Linux zu sein. Jedem Hobby- und Profiadmin den ich kenne war das Programm geläufig. Ich erntete für meine Unwissenheit des Öfteren ein müdes Lächeln.&lt;/p&gt;&#10;&lt;p&gt;Zu Installation und Konfiguration gibt es bereits einen Haufen Anleitungen da draußen plus der (sehr gut geschriebenen), die im Repo von Fail2ban gleich mitgeliefert wird. Daher gehe ich nicht besonders tief hierauf ein.&lt;/p&gt;&#10;&lt;p&gt;Ich entschied mich für die Installation im Docker-Container, damit ich die üblichen Abhängigkeiten gleich mitgeliefert bekomme. Dafür verwende ich die open-source Distribution von &lt;a href="https://docs.linuxserver.io/images/docker-fail2ban/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;linuxserver&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und verfasse die folgende &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /fail2ban/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;2.1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;services&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;fail2ban&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;lscr.io/linuxserver/fail2ban:latest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;fail2ban&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;cap_add&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;NET_ADMIN&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;NET_RAW&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;network_mode&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;host&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;PUID=1000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;PGID=1000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;TZ=Etc/UTC&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;VERBOSITY=-vv&lt;/span&gt; &lt;span style="color:#75715e"&gt;#optional&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./config:/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/var/log/auth.log:/var/log/auth.log:ro&lt;/span&gt; &lt;span style="color:#75715e"&gt;# host ssh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/var/log/caddy2:/var/log/caddy2:ro &lt;/span&gt; &lt;span style="color:#75715e"&gt;# gitea via caddy, caddyserver&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;restart&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;unless-stopped&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das Einzige, was hier zu beachten gilt: &lt;code&gt;fail2ban&lt;/code&gt; benötigt die Access logs per Volume zur Verfügung gestellt (oben als &lt;code&gt;:ro&lt;/code&gt; nur mit Lesezugriff angegeben). Eine Menge Filterregeln liegen bereits vorkonfiguriert im &lt;code&gt;config&lt;/code&gt;-Ordner, daher muss ich nur die &lt;code&gt;jail.local&lt;/code&gt; &lt;a href="https://github.com/linuxserver/fail2ban-confs" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nach &lt;code&gt;linuxserver/fail2ban-confs&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; sowie &lt;code&gt;filter.d&lt;/code&gt; für Caddy &lt;a href="https://muetsch.io/how-to-integrate-caddy-with-fail2ban.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nach muetsch.io&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; anpassen und schon habe ich einen automatischen Türwächter.&lt;/p&gt;&#10;&lt;h3 id="fail2ban-beispiel"&gt;fail2ban Beispiel&lt;/h3&gt;&#10;&lt;p&gt;So sieht ein fail2ban-Log nun für meinen SSH-Daemon aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# schallbert:/opt/fail2ban/config/log/fail2ban# grep &amp;#34;220.124.89.47&amp;#34; fail2ban.log &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;600&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;23&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;003&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;22&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;25&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;205&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;24&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;206&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;26&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;610&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;33&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;045&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB36104B38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NOTIC&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Ban&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und tschüss!&lt;/p&gt;&#10;&lt;h3 id="was-noch-nicht-funktioniert-gitea--fail2ban"&gt;Was (noch) nicht funktioniert: Gitea &amp;amp; fail2ban&lt;/h3&gt;&#10;&lt;p&gt;Ich bekomme auch auf meiner Gitea-Instanz ssh-Anfragen rein, die ich ebenfalls gern wegblocken möchte. Allerdings kann ich Gitea bis jetzt partout nicht dazu bekommen, die Logs dafür auch in eine Datei zu schreiben. Bis jetzt werden die stets an die Konsole geschickt, wo ich sie fail2ban natürlich nicht zuführen kann und möchte. Tatsächlich wird die Verbindung zu Gitea per SSH zum Glück bereits gesichert, da sie über denselben &lt;code&gt;sshd&lt;/code&gt; geht wie der Shell-Zugriff auf meinen Server selbst.&lt;/p&gt;&#10;&lt;p&gt;Was mir noch fehlt sind nur die Überwachung fehlgeschlagener Logins auf dem Web-Frontend.&lt;/p&gt;&#10;&lt;p&gt;Dabei sieht Giteas &lt;code&gt;app.ini&lt;/code&gt; für mich sauber aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/conf/app.ini&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[log]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;LEVEL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;warn&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ROOT_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLE_ACCESS_LOGS&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLE_SSH_LOG&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;logger.access.MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;access-file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[log.access-file]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ACCESS&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;LEVEL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;info&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;FILE_NAME&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich habe sowohl die Access-Logs aktiviert &lt;code&gt;ENABLE_ACCESS_LOGS&lt;/code&gt; und den Access-Logger in eine Datei schreiben lassen. Die Logs werden auch erstellt, nur sind da keine Zugriffe drin aufgelistet - diese gehen nach wie vor in die Konsole des Containers. Ich bin mir aber noch unsicher, ob der Reverse-Proxy von Caddy hier vielleicht einen Einfluss hat und er zum Beispiel die Zugriffe vor Gitea bereits abfängt. Aber das finde ich schon irgendwann noch heraus.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; fail2ban mit ipdables und banip überprüfen&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-08-22T00:00:00Z"&gt;&#10; 22. Aug. 2024&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Irgendwann fragte ich mich, ob &lt;em&gt;fail2ban&lt;/em&gt; auch tatsächlich auf den iptables meines Servers arbeitet oder nur innerhalb des Containers korrekt funktioniert. Zum Glück war ich &lt;a href="https://stackoverflow.com/questions/59996070/how-to-understand-if-the-fail2ban-ssh-filter-is-working-with-a-new-port" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nicht der Erste mit dieser Frage (stackoverflow)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und fand die untenstehende Lösung ganz charmant.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;im Docker-Container eine beliebige IP sperren (&lt;code&gt;docker exec -it fail2ban sh&lt;/code&gt;): &lt;code&gt;fail2ban-client set sshd banip 111.111.111.111&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;In den iptables nachsehen, ob diese IP dort auftaucht: &lt;code&gt;iptables -n -L --line-numbers&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;In meinem Falle: freuen, denn sie ist da: &lt;code&gt;1 REJECT all -- 111.111.111.111&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Die IP wieder entsperren &lt;code&gt;fail2ban-client set sshd unbanip 111.111.111.111&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; fail2ban für mehr als nur `ssh` verwenden&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-08-22T00:00:00Z"&gt;&#10; 22. Aug. 2024&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Ich habe jetzt eine Lösung für &lt;em&gt;Gitea&lt;/em&gt;, aber auch alle anderen meiner Websites zustande gebracht:&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schutz vor Brute-Force Angriffen per &lt;code&gt;ssh&lt;/code&gt; wie oben beschrieben&lt;/li&gt;&#10;&lt;li&gt;Schutz vor Überlast mittels &lt;a href="https://blog.schallbert.de/fail2ban-with-caddy/"&gt;Rate Limiter&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Schutz vor Angriffen auf die APIs (&lt;code&gt;404/403&lt;/code&gt; Attacken) mittels &lt;code&gt;caddy-status&lt;/code&gt; Konfiguration für fail2ban, Logs in &lt;code&gt;JSON&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /config/fail2ban/filter.d/caddy-status.conf&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# this regex works for caddy with json-style logs&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;failregex = &amp;#34;client_ip&amp;#34;:&amp;#34;&amp;lt;HOST&amp;gt;&amp;#34;(.*)&amp;#34;status&amp;#34;:(400|401|403|404|500)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;datepattern = \d+&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;ignoreregex =&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="regelmäßige-backups"&gt;Regelmäßige Backups&lt;/h2&gt;&#10;&lt;p&gt;Ein wichtiger Aspekt für mich ist die Möglichkeit, den Server wiederherstellen zu können. Sollten unvorhergesehene Ereignisse eintreten wie ein Update einer Komponente welches die Funktion anderer Programme beeinträchtigt, der Ausfall des Servers, oder sogar der Zugriff fremder Personen auf selbigen - in jedem Falle könnte ich mit einem Backup recht schnell eine neue Instanz des Servers erzeugen, konfigurieren und die Website wieder zum Laufen bringen.&lt;/p&gt;&#10;&lt;p&gt;Diese Updates möchte ich aber ungern selbst von Hand aus meinen Ordnern erzeugen, komprimieren und per &lt;code&gt;SCP / SFTP&lt;/code&gt; herunterladen. Besser soll das vollautomatisch vonstatten gehen, und das im Optimalfall noch kostenfrei. Nach einer kurzen Recherche stellen sich für meine Zwecke die quelloffenen Werkzeuge &lt;a href="https://torsion.org/borgmatic/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;borgmatic&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und &lt;a href="https://restic.net/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;restic&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; als geeignet heraus.&lt;/p&gt;&#10;&lt;p&gt;Ich entscheide mich willkürlich für Borgmatic.&lt;/p&gt;&#10;&lt;h3 id="borgmatic-in-docker-installieren"&gt;Borgmatic in Docker installieren&lt;/h3&gt;&#10;&lt;p&gt;Wie bei allen anderen Komponenten auch, möchte ich Borgmatic in einem Container laufen lassen. Glücklicherweise gibt es da bereits &lt;a href="https://hub.docker.com/r/b3vis/borgmatic/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;eine fertige Lösung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die ich nur noch ein wenig konfigurieren muss.&lt;/p&gt;&#10;&lt;p&gt;Mein &lt;code&gt;docker-compose&lt;/code&gt; File für Borgmatic sieht wie folgt aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /borgmatic/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;3&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;services&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;borgmatic&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ghcr.io/borgmatic-collective/borgmatic&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;borgmatic&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;${VOLUME_SOURCE}:/mnt/source:ro &lt;/span&gt; &lt;span style="color:#75715e"&gt;# backup source&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;${VOLUME_TARGET}:/mnt/repository &lt;/span&gt; &lt;span style="color:#75715e"&gt;# backup target&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;${VOLUME_ETC_BORGMATIC}:/etc/borgmatic.d/ &lt;/span&gt; &lt;span style="color:#75715e"&gt;# borgmatic config file(s) + crontab.txt&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;${VOLUME_BORG_CONFIG}:/root/.config/borg &lt;/span&gt; &lt;span style="color:#75715e"&gt;# config and keyfiles&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;${VOLUME_SSH}:/root/.ssh &lt;/span&gt; &lt;span style="color:#75715e"&gt;# ssh key for remote repositories&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;${VOLUME_BORG_CACHE}:/root/.cache/borg &lt;/span&gt; &lt;span style="color:#75715e"&gt;# checksums used for deduplication&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# - /var/run/docker.sock:/var/run/docker.sock # add docker sock so borgmatic can start/stop containers to be backupped&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;TZ=${TZ}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;BORG_PASSPHRASE=${BORG_PASSPHRASE}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;restart&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;always&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Als Inspiration hierfür habe ich reichlich in der &lt;a href="https://github.com/borgmatic-collective/docker-borgmatic/blob/master/README.md" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dokumentation auf Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gestöbert.&lt;/p&gt;&#10;&lt;h3 id="borgmatic-konfigurieren"&gt;Borgmatic konfigurieren&lt;/h3&gt;&#10;&lt;p&gt;Alle konkreten Daten (die Angaben in &lt;code&gt;${}&lt;/code&gt;) habe ich zur besseren Übersichtlichkeit in einer &lt;code&gt;.env&lt;/code&gt;-Datei im selben Verzeichnis abgelegt. Ganz nebenbei verhindere ich, aus Versehen Passphrases zu veröffentlichen. Diese Passphrase habe ich mir zusätzlich auf einem Zettel notiert - man weiß ja nie, ob man sie nochmal benötigt.&lt;/p&gt;&#10;&lt;p&gt;Ich muss anschließend nur noch die sich in dem Ordner &lt;code&gt;borgmatic.d/&lt;/code&gt; befindliche &lt;code&gt;config.yml&lt;/code&gt; leicht verändern, indem ich Backup-Quelle, -Ziel und die von mir gewünschten Zeitintervalle für die Sicherungserstellung eintrug und war quasi schon bereit für einen ersten Test.&lt;/p&gt;&#10;&lt;h3 id="borgmatic-funktionstest"&gt;Borgmatic Funktionstest&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker exec borgmatic bash -c &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;cd &amp;amp;&amp;amp; borgmatic --stats -v 1 --files 2&amp;gt;&amp;amp;1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mit diesem Befehl führe ich über Docker im Container &lt;code&gt;borgmatic&lt;/code&gt; den Befehl zum Anlegen einer Sicherung an, um die Funktion von Borgmatic zu verifizieren.&lt;/p&gt;&#10;&lt;p&gt;Hier kam dann direkt eine Fehlermeldung à la &lt;code&gt;repository does not exist&lt;/code&gt; zurück. Also existiert das Backup-Ziel noch gar nicht. Ein kurzer Blick in die Dokumentation zeigt, dass das Repository erst initialisiert werden muss.&lt;/p&gt;&#10;&lt;p&gt;Dies hole ich mit dem Kommando&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker exec borgmatic bash -c &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;borgmatic init --encryption repokey-blake2&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;nach, was mit ein &amp;ldquo;leeres&amp;rdquo;, verschlüsseltes Repository anlegt.&#10;Versuche ich erneut, ein Backup zu erzeugen, läuft Borgmatic nun durch und erzeugt mir das Backup im Repository.&lt;/p&gt;&#10;&lt;h3 id="ein-backup-ist-nur-ein-backup"&gt;Ein Backup ist nur ein Backup&amp;hellip;&lt;/h3&gt;&#10;&lt;p&gt;&amp;hellip;wenn man es einspielen kann, sagt mein Kumpel.&lt;/p&gt;&#10;&lt;p&gt;Er hat Recht, aber so richtig traue ich mich nicht, über meine funktionierende Serverkonfiguration drüberzubügeln. Also erstelle ich mir eine &lt;code&gt;docker-compose.restore.yml&lt;/code&gt; Datei nach &lt;a href="https://github.com/borgmatic-collective/docker-borgmatic/blob/master/docker-compose.restore.yml" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Vorbild auf dem Repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und ziehe &lt;a href="https://www.modem7.com/books/docker-backup/page/backup-docker-using-borgmatic" target="_blank" rel="noopener noreferrer" class="external-link"&gt;modem7s Anleitung zum Thema&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; hinzu.&lt;/p&gt;&#10;&lt;p&gt;Tatsächlich kann ich durch Ausführen der &lt;code&gt;docker-compose.restore.yml&lt;/code&gt; dann in der Container-Shell folgende Kommandos ausführen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir backuprestoremount&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;borg mount /mnt/repository /backuprestoremount&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir backuprestore&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;borgmatic extract --archive latest --destination /backuprestore&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hier erstelle ich den Ordner &lt;code&gt;backuprestoremount&lt;/code&gt; und lasse ihn von &lt;code&gt;borg&lt;/code&gt; auf mein Backup zeigen. Anschließend extrahiere ich das Backup in den ebenfalls neu erstellten Ordner &lt;code&gt;backuprestore&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Nun prüfe ich, ob auch &amp;ldquo;alles da ist&amp;rdquo;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# cd /mnt/backuprestore&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /restore/mnt/source ls&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;borgmatic caddy2 containerd fail2ban gitea hostedtoolcache watchtower&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Yeah, alle Applikationen sind vorhanden und die enthaltenen Daten sind gesichert! Weiter gehe ich jetzt mal nicht, denn ich bin zu feige (und zu faul), die auf dem Server unter &lt;code&gt;/opt&lt;/code&gt; befindlichen Container zu überschreiben.&lt;/p&gt;&#10;&lt;h3 id="was-noch-nicht-funktioniert-borgmatic--docker-compose-down"&gt;Was (noch) nicht funktioniert: Borgmatic &amp;amp; docker-compose down&lt;/h3&gt;&#10;&lt;p&gt;Borgmatic bietet eine einfache Möglichkeit in der Konfigurationsdatei, Aktionen vor- und nach dem Backup auszuführen.&lt;/p&gt;&#10;&lt;p&gt;Da ich die Konsistenz der Datenbanken meiner zu sichernden Container sicherstellen möchte, sollten die Container zum Zeitpunkt des Backups heruntergefahren sein. Daher schrieb ich mir ein Skript, was alle Applikationen vor dem Backup per &lt;code&gt;docker compose down&lt;/code&gt; stoppt und nach dem Backup wieder hochfährt.&lt;/p&gt;&#10;&lt;p&gt;Dies würde auch alles prima funktionieren, wenn Borgmatic nicht selbst in einem Container liefe. Durch die Kapselung bin ich nun nicht in der Lage, die im Stamm-Dateisystem der Maschine befindlichen Skripte auszuführen. Kurzzeitig dachte ich, dass ich dies mittels Durchreichung von &lt;code&gt;var/run/docker.sock&lt;/code&gt; in den Borgmatic-Container umgehen könne, scheiterte dann aber an der Ankopplung an Docker compose.&lt;/p&gt;&#10;&lt;p&gt;Ich bin mir sicher, dass das zu lösen ist. Allerdings habe ich gerade dringendere Themen. Daher begnüge ich mich mit der Annahme, dass schon nichts passiert, wenn ich das Backup nachts ziehe&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#10;&lt;p&gt;Auch bei den Auto-Backups scheint es noch zu haken:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;/ # borgmatic --stats&#10;Starting a backup job.&#10;Failed to create/acquire the lock /mnt/repository/lock (timeout).&#10;local: Error running actions for repository&#10;Command &amp;#39;borg create --stats /mnt/repository::{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f} /etc/borgmatic.d/config.yml /mnt/source /root/.borgmatic&amp;#39; returned non-zero exit status 2.&#10;Error while creating a backup.&#10;/etc/borgmatic.d/config.yml: An error occurred&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Dies entnehme ich der über Docker aufgerufenen Log-Konsole von Borgmatic. Ich muss also beizeiten nochmal um meine Backups kümmern.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Beim SSH-Daemon meines Servers liegen die Zugriffslogs z.B. in &lt;code&gt;/var/log/auth.log&lt;/code&gt;, können aber auch als &lt;code&gt;access.log&lt;/code&gt; oder ähnlich abgelegt werden. Auch von mir verwendete Dienste wie Gitea und Caddy legen solche Logdateien an.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;Was ich allerdings noch nicht verstehe ist, wie &lt;code&gt;fail2ban&lt;/code&gt; im Container laufend überhaupt an die iptables herankommt. Ich dachte, dass ein positiver Nebeneffekt der Containerisierung in der Kapselung liegt. Oder ist sie nur bei &amp;ldquo;rootless&amp;rdquo;-Containern zu erreichen?&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;Hintergrund: Ich bin zur Zeit der Einzige, der Inhalte auf &lt;code&gt;gitea&lt;/code&gt; hochlädt. Meine Webseite ist statisch. Auf dem Webserver &lt;code&gt;caddy&lt;/code&gt; ändern sich also nur dann Dateien, wenn ich sie über Gitea hochlade. Programmupdates über &lt;code&gt;watchtower&lt;/code&gt; und &lt;code&gt;unattended-upgrades&lt;/code&gt; &lt;a href="https://blog.schallbert.de/server-auto-upgrade/"&gt;(Thema eines weiteren Artikels)&lt;/a&gt; habe ich so gelegt, dass sie nicht während des Durchlaufs von &lt;code&gt;borgmatic&lt;/code&gt; stattfindet. Einzig die Logs, welche bei &lt;code&gt;fail2ban&lt;/code&gt; aufschlagen, werden auch zur Zeit der Erstellung des Backups geschrieben. Das Risiko des Datenverlustes gehe ich an der Stelle bewusst aber ein, da die Paketfilterung für geblockte IP-Adressen durch fail2ban nach einer von mir bestimmten Zeit sowieso wieder aufgehoben wird.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Jekyll build: defekte relative Links</title><link>https://blog.schallbert.de/self-hosted-jekyll-page-broken-links/</link><pubDate>Fri, 29 Dec 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/self-hosted-jekyll-page-broken-links/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-29_serves_broken_links-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Webpage, displaying Schallbert&amp;#39;&amp;#39;s Blog with broken links"&#10; title="Jekyll build: defekte relative Links" /&gt;&#10;&lt;h2 id="das-problem"&gt;Das Problem&lt;/h2&gt;&#10;&lt;p&gt;Jetzt bin ich nach &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/"&gt;ewigem Probieren&lt;/a&gt; nun endlich in der Lage, meinem Caddyserver die gebauten Dateien per Docker-Volume zur Verfügung zu stellen. Leider sieht die Seite aber wie folgt aus:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-29_serves_broken_links.jpg" alt="Image: Broken webpage, displaying Schallbert&amp;#39;s Blog without pictures, and other media, and any links lead to 404-nowhere"&gt;&lt;/figure&gt;&#10;&lt;h2 id="die-analyse"&gt;Die Analyse&lt;/h2&gt;&#10;&lt;p&gt;Zuerst hatte ich den Webserver in Verdacht.&#10;Wenn ich die Website aber testweise in meinem Browser aufrufe&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;, zeigt sich dasselbe Verhalten. Merkwürdig.&lt;/p&gt;&#10;&lt;p&gt;Offensichtlich fehlen sämtliche Stylesheets. Sieht man genau hin, sind aber auch Bilder und andere Medien nicht geladen worden, sondern lediglich ihre Beschreibung.&#10;Dann klicke ich wild auf ein paar Links zu Artikeln und Unterseiten. Dabei fällt auf, dass die Pfade anders aussehen, als ich erwartet hätte:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# --- Erwartung ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Bilddatei&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;/assets/images/test.jpg&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Artikel&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;/this-is-a-post&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Stattdessen sehe ich:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# --- Beobachtung ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Bilddatei&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;/pages/&amp;lt;username&amp;gt;/&amp;lt;repositoryname&amp;gt;/assets/images/test.jpg&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Artikel&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;/pages/&amp;lt;username&amp;gt;/&amp;lt;repositoryname&amp;gt;/this-is-a-post&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dies zeigen auch die mit &lt;code&gt;F12&lt;/code&gt; zugeschalteten Entwickleroptionen des Browsers (siehe Bild oben): sämtliche per relativen Link eingebundene Quellen können nicht geladen werden. Übrigens ist es im Allgemeinen eine klare Empfehlung, die Entwickleroptionen bei Problemen mit der Darstellung von Webseiten aktiviert zu haben.&lt;/p&gt;&#10;&lt;p&gt;Liegt ja irgendwie nahe, dass meine Jekyll-Konfigurationsdatei &lt;code&gt;_config.yml&lt;/code&gt; oder meine &lt;a href="https://blog.schallbert.de/jekyll-polyglot-language-support/"&gt;Übersetzungsheader&lt;/a&gt; &lt;code&gt;i10.yml&lt;/code&gt; Fehler enthalten. Dafür gibt es zum Glück bereits hervorragende &lt;a href="https://mademistakes.com/mastering-jekyll/site-url-baseurl/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hilfeseiten&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Ein Test zeigt jedoch schnell, dass ich die Angabe &lt;code&gt;repository&lt;/code&gt;, woher der Build &lt;code&gt;&amp;lt;username&amp;gt;/&amp;lt;repositoryname&amp;gt;&lt;/code&gt; herbezieht, nicht einfach leer lassen kann:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# bundle exec jekyll serve&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Generating... &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Jekyll Feed: Generating feed &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; posts&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Liquid Exception: No repo name found. &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Specify using PAGES_REPO_NWO environment variables, &lt;span style="color:#e6db74"&gt;&amp;#39;repository&amp;#39;&lt;/span&gt; in your configuration, &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; or set up an &lt;span style="color:#e6db74"&gt;&amp;#39;origin&amp;#39;&lt;/span&gt; git remote pointing to your github.com repository. in /_layouts/default.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ERROR: YOUR SITE COULD NOT BE BUILT&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und, verflixt, wo kommt der Unterordner &lt;code&gt;/pages&lt;/code&gt; plötzlich her?&lt;/p&gt;&#10;&lt;h2 id="die-lösung"&gt;Die Lösung&lt;/h2&gt;&#10;&lt;p&gt;Durch das &lt;code&gt;/pages&lt;/code&gt; im Pfad liegt die Vermutung nahe, dass die Ursache in einer Kombination aus &lt;a href="https://jekyllrb.com/docs/configuration/options/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Jekyll-Bauoptionen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und dem &lt;a href="https://github.com/github/pages-gem" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github-Pages Gem&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zu finden ist.&lt;/p&gt;&#10;&lt;p&gt;Natürlich bin ich mit diesem Problem nicht allein, und nach längerer Recherche finde ich &lt;a href="https://stackoverflow.com/questions/51869314/jekyll-serve-generate-wrong-path-in-localhost" target="_blank" rel="noopener noreferrer" class="external-link"&gt;tatsächlich eine Dokumentation der Lösung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Also ändere ich die Umgebungsvariable &lt;code&gt;JEKYLL_ENV&lt;/code&gt; (steht für die Bauumgebung) von &lt;code&gt;production&lt;/code&gt; auf &lt;code&gt;development&lt;/code&gt;, so wie es normalerweise für das lokale Bauen vorgesehen ist.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# workflows/jekyll-build-action.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;env&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;JEKYLL_ENV&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;development&lt;/span&gt; &lt;span style="color:#75715e"&gt;# I had &amp;#34;production&amp;#34; here before&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und, was soll ich sagen: Kaum macht man es richtig, schon funktioniert&amp;rsquo;s!&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-29_css_assets_show_correctly.jpg" alt="Image: blog.schallbert.de shows correct CSS and media are loaded along working links"&gt;&lt;/figure&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Die Website offline darstellen funktioniert wie folgt: Die gebauten Dateien (den &lt;code&gt;_site&lt;/code&gt;-Ordner) auf den eigenen Rechner ziehen und die &lt;code&gt;index.html&lt;/code&gt; mit dem Browser öffnen.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>🗸 Gitea Actions Teil2 - Jekyll-Dockerimage</title><link>https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/</link><pubDate>Sat, 09 Dec 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-09_gitea_infrastructure-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: giteas infrastructure in a nutshell"&#10; title="🗸 Gitea Actions Teil2 - Jekyll-Dockerimage" /&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Gitea schickt `act_runner` in Rente&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-09-15T00:00:00Z"&gt;&#10; 15. Sept. 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Dieser Artikel bezieht sich auf eine Actions-Implementierung von Gitea, den &lt;code&gt;act_runner&lt;/code&gt;. Jener ist abgeleitet von &lt;a href="https://github.com/nektos/act" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nectos/act&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Gitea verwendet inzwischen &lt;a href="https://blog.gitea.com/release-of-runner-1.0.0/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;einen eigenen Runner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Der alte Runner sollte ausgetauscht werden. Weitere Infos: Blogartikel &lt;a href="https://blog.schallbert.de/build-deploy-hugo-with-actions-docker-caddy/"&gt;Hugo-Seite mit Gitea Actions, docker, und caddy bereitstellen&lt;/a&gt;&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="die-vorgeschichte"&gt;Die Vorgeschichte&lt;/h2&gt;&#10;&lt;p&gt;Leider hatte ich im &lt;a href="https://blog.schallbert.de/gitea-action-runner-native-jekyll/"&gt;Teil1&lt;/a&gt; mit einer leicht modifizierten Kopie der Github-Action zum Bauen von Jekyll kein Glück. Daher wollte ich nun probieren, Jekyll direkt als Dockerimage laufen zu lassen. Im Docker hub gab es nicht so furchtbar viel Auswahl. Trotzdem habe ich ein (in 2023) aktiv gewartetes und gut dokumentiertes Image finden können.&lt;/p&gt;&#10;&lt;h2 id="set-up-job"&gt;&amp;ldquo;Set up job&amp;rdquo;&lt;/h2&gt;&#10;&lt;p&gt;Und so binde ich das Docker-Jekyllimage dann in meinen workflow ein:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# workflows/jekyll-build-action.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;jobs&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Build job&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;build&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt; &lt;span style="color:#75715e"&gt;# this is the &amp;#34;label&amp;#34; the runner will use and map to docker target OS&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jvconseil/jekyll-docker&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hier wird dem &lt;code&gt;gitea act_runner&lt;/code&gt; gesagt, dass er mit dem &lt;code&gt;ubuntu-latest&lt;/code&gt; label laufen soll, was in meinem Fall auf eine sehr kleine und schnell hochzufahrende &lt;code&gt;node16:bullseye&lt;/code&gt; Maschine (abgeleitet vom Betriebssystem Debian11) gemappt wird. Zusätzlich wird Docker mitgeteilt, dass bitte das &lt;code&gt;jekyll-docker&lt;/code&gt; image zu laden ist, wo die von mir benötigten Abhängigkeiten (Ruby, Sass, Jekyll&amp;hellip;) bereits berücksichtigt wurden.&lt;/p&gt;&#10;&lt;h2 id="bundle-install"&gt;bundle install&lt;/h2&gt;&#10;&lt;p&gt;Das Bauen schlägt hier allerdings fehl wegen in der &lt;code&gt;Gemfile&lt;/code&gt; angegebenen, aber nicht installierten Abhängigkeiten. Das kann dann so ählich aussehen wie hier:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;bundler: failed to load command: jekyll (/usr/gem/bin/jekyll)&#10;/usr/local/lib/ruby/gems/3.2.0/gems/bundler-2.4.22/lib/bundler/resolver.rb:332:in `raise_not_found!&amp;#39;: Could not find gem &amp;#39;github-pages&amp;#39; in locally installed gems. (Bundler::GemNotFound)&#10;&#9;from /usr/local/lib/ruby/gems/3.2.0/gems/bundler-2.4.22/lib/bundler/resolver.rb:392:in `block in prepare_dependencies&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://bundler.io" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Bundler&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist der Paketmanager für in Ruby geschriebene Software. Ähnlich wie &lt;code&gt;npm&lt;/code&gt; für Javascript oder &lt;code&gt;pip&lt;/code&gt; für Python ist &lt;code&gt;bundler&lt;/code&gt; in der Lage, von einem Ruby-Programm genutzte Bibliotheken und andere Abhängigkeiten herunterzuladen und so einzubinden, dass sie ohne weitere Konfiguration auf dem Zielsystem verwendet werden können. Damit &lt;code&gt;bundler&lt;/code&gt; weiß, was zu installieren ist, wird eine sogenannte &lt;code&gt;Gemfile&lt;/code&gt; angelegt.&lt;/p&gt;&#10;&lt;p&gt;Also dem Script schnell &lt;code&gt;bundle install&lt;/code&gt; hinzufügen, etwa so:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# workflows/jekyll-build-action.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;CHECKOUT ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v3&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;INSTALL GEMS ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;bundle install&lt;/span&gt; &lt;span style="color:#75715e"&gt;# will fail with permissions rights to write to Gemfile.lock but anyways installs required dependencies.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;BUILD WITH JEKYLL ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Outputs to the &amp;#39;./_site&amp;#39; directory by default&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;bundle exec jekyll build --destination /opt/blog_staging&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;env&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;JEKYLL_ENV&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;production&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="act_runner-kein-schreibzugriff-auf-gemfilelock"&gt;act_runner: Kein Schreibzugriff auf &lt;code&gt;Gemfile.lock&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Dies schlägt wieder fehl - wegen fehlender Schreibrechte des &lt;code&gt;act_runner&lt;/code&gt;:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;gitea-runner-1 | [Deploy Jekyll site/build] | `/workspace/schallbert/blog/Gemfile.lock`. It is likely that you need to grant&#10;gitea-runner-1 | [Deploy Jekyll site/build] | write permissions for that path.&#10;gitea-runner-1 | [Deploy Jekyll site/build] ❌ Failure - Main ---INSTALL GEMS ---&#10;gitea-runner-1 | [Deploy Jekyll site/build] exitcode &amp;#39;23&amp;#39;: failure&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Nach langer Suche muss ich feststellen, dass &lt;code&gt;Gemfile.lock&lt;/code&gt; auf meinem Rechner nicht der Versionskontrolle unterliegt, da es in der &lt;code&gt;.gitignore&lt;/code&gt; auftaucht. Der Bundler des &lt;code&gt;act_runner&lt;/code&gt; versucht nun also, diese Datei selbst aus der &lt;code&gt;Gemfile&lt;/code&gt; zu erzeugen und scheitert, weil keine Schreibrechte auf dem Repository bzw. der temporären Kopie vorliegen.&lt;/p&gt;&#10;&lt;p&gt;Dies lässt sich am besten beheben, indem die &lt;code&gt;Gemfile.lock&lt;/code&gt; in die Versionskontrolle aufgenommen wird, d.h. aus der &lt;code&gt;.gitignore&lt;/code&gt; verschwindet. Somit stelle ich sicher, dass ich für das lokale Bauen auf meinem Laptop dieselben Umgebungsbedingungen habe wie auf dem Server.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Probleme mit dem Bundle beheben&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2025-11-04T00:00:00Z"&gt;&#10; 04. Nov. 2025&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Nach einem Update meiner lokalen Bildumgebung per &lt;code&gt;bundle update&lt;/code&gt; stellt sich bei mir nun erneut das oben genannte Problem, obwohl ich &lt;code&gt;Gemfile.lock&lt;/code&gt; inzwischen versioniere. Die Lösung diskutiere ich in einem &lt;a href="https://blog.schallbert.de/bundler-ci-gemfile-issue/"&gt;Artikel zum Thema Bundler&lt;/a&gt;.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h3 id="abhängigkeiten-sass-embedded"&gt;Abhängigkeiten: &lt;code&gt;sass-embedded&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Nach weniger als 20 Sekunden die nächste Fehlermeldung:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;Resolving dependencies...&#10;Could not find gem &amp;#39;sass-embedded (= 1.69.5)&amp;#39; with platform &amp;#39;x86_64-linux&amp;#39; in&#10;rubygems repository https://rubygems.org/ or installed locally.&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Nach weiteren Minuten der Recherche die &lt;a href="https://github.com/helaili/jekyll-action/issues/150#issuecomment-1374388728" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Lösung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;: In der Gemfile eine bestimmte Version des &lt;code&gt;jekyll-sass-converter&lt;/code&gt; spezifizieren oder alternativ das Gem &amp;ldquo;github-pages&amp;rdquo; in der Gemfile belassen. Dieses kümmert sich ebenfalls darum, dass der CascadingStyleSheets (CSS) Präprozessor &lt;a href="https://sass-lang.com/guide/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Sass&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; vorhanden ist.&lt;/p&gt;&#10;&lt;p&gt;Nun endlich läuft &lt;code&gt;bundle install&lt;/code&gt; erfolgreich durch:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;✅ Bundle complete! 5 Gemfile dependencies, 43 gems now installed.&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="jekyll-build"&gt;jekyll build&lt;/h2&gt;&#10;&lt;p&gt;Damit Jekyll die gebauten Dateien auch irgendwo abspeichern kann, habe ich dem &lt;code&gt;act_runner&lt;/code&gt; in der &lt;code&gt;docker-compose.yml&lt;/code&gt; ein weiteres &lt;a href="https://docs.docker.com/storage/volumes/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Volume&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gegönnt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./runner/blog_staging:/opt/blog_staging&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./runner/data:/data&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und jetzt direkt nochmal probieren, ob Jekyll erfolgreich baut:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; Destination: /opt/blog_staging&#10; Generating... &#10; Jekyll Feed: Generating feed for posts&#10;jekyll 3.9.3 | Error: Permission denied @ dir_s_mkdir - /opt/blog_staging&#10;/usr/local/lib/ruby/3.2.0/fileutils.rb:406:in `mkdir&amp;#39;: Permission denied @ dir_s_mkdir - /opt/blog_staging (Errno::EACCES)&#10;&#9;from /usr/local/lib/ruby/3.2.0/fileutils.rb:406:in `fu_mkdir&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Auch dieses Problem lässt sich leicht lösen, indem man entweder nach &lt;code&gt;/tmp&lt;/code&gt; baut, wo der user &lt;code&gt;jekyll&lt;/code&gt; während des Bauens ebenfalls Zugriff hat, oder indem man der Action vor dem &lt;code&gt;jekyll build&lt;/code&gt;-Befehl einen Nutzerwechsel im Build-Arbeitsverzeichnis vornimmt: &lt;code&gt;chown -R jekyll /opt/blog_staging&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Mit dieser Änderung läuft die Action endlich durch.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-09_buildsuccessful.jpg" alt="Image: Gitea snapshot showing a successful build"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h2 id="artefakte-transferieren"&gt;Artefakte transferieren&lt;/h2&gt;&#10;&lt;p&gt;Doch wo finde ich jetzt die in der Action gebauten Dateien, welche mein Webserver dann veröffentlichen soll? Der &lt;code&gt;_site&lt;/code&gt;-Ordner ist nirgendwo zu finden - weder auf dem Volume der Hostmaschine, noch im Gitea-Dockercontainer oder im Runner!&lt;/p&gt;&#10;&lt;h3 id="docker-volumes-für-act_runner-können-artefakte-nicht-verteilen"&gt;Docker volumes für &lt;code&gt;act_runner&lt;/code&gt; können Artefakte nicht verteilen&lt;/h3&gt;&#10;&lt;p&gt;Dass ich über Docker volumes nicht dort herankomme erscheint logisch wenn man weiß, wie &lt;code&gt;act-runner&lt;/code&gt; funktioniert: Die Action erzeugt ein eigenes Dockerimage mit wiederum per Task-ID assoziierten Volumes, welches nur für dessen Laufzeit existiert.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;DRIVER VOLUME NAME&#10;--&amp;gt; local GITEA-ACTIONS-TASK-84_WORKFLOW-Deploy-Jekyll-site_JOB-build &amp;lt;--&#10;--&amp;gt; local GITEA-ACTIONS-TASK-84_WORKFLOW-Deploy-Jekyll-site_JOB-build-env &amp;lt;--&#10;local act-toolcache&#10;local blog_staging&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Legt man die Artefakte also nirgendwo extern ab, so werden sie nach Durchlauf der Aktionen zusammen mit dem Image demontiert und sind futsch. Das &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/#jekyll-build"&gt;oben&lt;/a&gt; angelegte Volume des Runners hat gar keine Auswirkung auf die Action, weil sie wieder in einem isolierten Image läuft.&lt;/p&gt;&#10;&lt;p&gt;Dies zu verstehen hat mich einige Zeit und Fehlversuche gekostet, dabei ist es &lt;a href="https://docs.gitea.com/usage/actions/act-runner?_highlight=runner#register-the-runner-with-docker" target="_blank" rel="noopener noreferrer" class="external-link"&gt;in einem Abschnitt der Dokumentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; beschrieben.&lt;/p&gt;&#10;&lt;p&gt;Das Ganze gestaltet sich ja deutlich komplizierter als ich dachte. Mit Kanonen auf Spatzen schießen angesichts meiner kleinen Website und als Einziger, der sie pflegt. Egal, ich ziehe das jetzt durch.&lt;/p&gt;&#10;&lt;h3 id="aber-man-kann-doch-upload-artifact-verwenden"&gt;Aber man kann doch &lt;code&gt;upload-artifact&lt;/code&gt; verwenden!&lt;/h3&gt;&#10;&lt;p&gt;Na ja, das dachte ich jedenfalls. Also habe ich mein Action-Skript entsprechend erweitert und die von Github Actions zur Verfügung gestellte Funktion eingebaut:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# workflows/jekyll-build-action.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Automatically upload the build folder to Giteas blog repo folder&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;UPLOAD ARTIFACT ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/upload-artifact@v3&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/workspace/schallbert/blog/&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Blog_Staging&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;retention-days&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Super, jetzt erscheint ein herunterladbares Zip-file auf der Weboberfläche von Gitea:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-09_uploadsuccessful.jpg" alt="Image: Gitea snapshot showing a successful artifact upload"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Nur: Wie bekomme ich das Ding jetzt automatisiert auf meine Serverinstanz? Schließlich kennt die Action diese nicht - sie hat ja nicht einmal einen eigenen Docker Daemon.&lt;/p&gt;&#10;&lt;p&gt;Also schaue ich mal im Gitea-Container selbst nach, wo die Artefakte unter &lt;code&gt;/data/gitea/actions_artifacts/BUILD_ID&lt;/code&gt; abgelegt sind. Allerdings als ein Haufen (hunderte) &lt;code&gt;.chunk.gz&lt;/code&gt; Dateien mit kryptischen Nummernfolgen als Name, bei denen ich wiederum nicht weiß wie ich sie zu einem Archiv zusammenfügen soll.&lt;/p&gt;&#10;&lt;p&gt;Und noch etwas gefällt mir an &lt;code&gt;upload-artifact&lt;/code&gt; nicht: Da meine Website inzwischen eine Menge Bilder, Audiodateien und Videos enthält, ist sie recht groß. Daher benötigt der uploader für&amp;rsquo;s Verpacken und Versenden schon jetzt &lt;em&gt;fast zwei Minuten&lt;/em&gt; - Tendenz linear steigend mit der Anzahl meiner Medieninhalte.&lt;/p&gt;&#10;&lt;p&gt;Sackgasse.&lt;/p&gt;&#10;&lt;h3 id="auch-docker-cp-funktioniert-in-diesem-falle-nicht"&gt;Auch Docker &lt;code&gt;cp&lt;/code&gt; funktioniert in diesem Falle nicht&lt;/h3&gt;&#10;&lt;p&gt;Dann versuche ich mit Hilfe des Kopierbefehls von Docker, die Artefakte auf meinen Host zu transferieren:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# workflows/jekyll-build-action.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;COPY ARTIFACT ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;docker cp gitea-runner-1:/workspace/schallbert/blog /tmp/blog_staging&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dies schlägt wieder fehl:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | docker cp gitea-runner-1:/workspace/schallbert/blog /tmp/blog_staging&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; | /var/run/act/workflow/3.sh: line 2: docker: not found&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ❌ Failure - Main --- COPY ARTIFACT ---&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Auch erwartbar, denn der Action-Container selbst kennt kein Docker. Es ist also nicht möglich, etwas von &amp;ldquo;innerhalb&amp;rdquo; des Containers per Docker heraus zu bekommen. Dies ist für mich aber erforderlich, da die Action ja den ganzen Prozess vollständig automatisieren soll.&lt;/p&gt;&#10;&lt;p&gt;Oder geht es doch irgendwie? Der Daemon der Hostmaschine &lt;code&gt;docker.sock&lt;/code&gt; steht laut Giteas Dokumentation zumindest dem &lt;code&gt;act_runner&lt;/code&gt; per Volume zur Verfügung - wird aber scheinbar nicht in die Action weitergeleitet. Vielleicht aus gutem Grund, sonst wäre die Kapselung futsch und eine schadhafte Action könnte praktisch meinen Docker Daemon auf dem Host kapern 😰.&lt;/p&gt;&#10;&lt;h3 id="transferieren-per-sftp-würde-klappen"&gt;Transferieren per SFTP würde klappen&lt;/h3&gt;&#10;&lt;p&gt;So langsam gehen mir die Alternativen aus. Ich überlege nun, die Artefakte per &lt;a href="https://de.wikipedia.org/wiki/SSH_File_Transfer_Protocol" target="_blank" rel="noopener noreferrer" class="external-link"&gt;SFTP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; aus dem vom &lt;code&gt;act_runner&lt;/code&gt; erzeugten Action-Dockerimage heraus in den Webserver-Container zu schieben&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-09_sftp_infrastructure.jpg" alt="Image: Machine architecture if I used SFTP for artifact share between Gitea and webserver"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Glücklicherweise gibt es hierfür wieder eine vorgefertigte Action: &lt;a href="https://github.com/marketplace/actions/scp-files" target="_blank" rel="noopener noreferrer" class="external-link"&gt;scp-files&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Nun muss ich aber den Container des Webservers so umbauen, dass er per SCP erreichbar ist und den SSH-Schlüssel der Action akzeptiert.&lt;/p&gt;&#10;&lt;p&gt;Andererseits wird davor &lt;a href="https://stackoverflow.com/questions/65381311/run-sshd-in-docker-container" target="_blank" rel="noopener noreferrer" class="external-link"&gt;in vielen Foren&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gewarnt - Ein Container sollte nur eine Applikation beherbergen. Aber auch hier gibt es eine Lösung: &lt;a href="https://hub.docker.com/r/panubo/sshd/#!" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ein eigener Docker-Container mit SSH-Daemon&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Diesem könnte ich dann ein Volume geben, welches er mit dem Webserver teilt.&lt;/p&gt;&#10;&lt;p&gt;Sieht leider nach einigem Aufwand aus. Und ich möchte als fauler Entwickler mein System so einfach wie möglich halten - also recherchiere ich weiter.&lt;/p&gt;&#10;&lt;h3 id="action-volumes"&gt;Dem Action-Container ein Volume zur Verfügung stellen&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-09_gitea_infrastructure.jpg" alt="Image: Docker-Gitea infrastructure overview"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Diese ganzen Fehlschläge und Sackgassen begleiten mich jetzt schon seit über einem Monat. Ich will es trotzdem unbedingt schaffen, vollautomatisch bauen, testen und veröffentlichen zu können. Auf dem Weg hierhin habe ich eine Menge gelernt und jetzt hoffe ich, dass dieser Ansatz mich endlich zur Lösung bringt.&lt;/p&gt;&#10;&lt;p&gt;Bei &lt;a href="https://gitea.com/gitea/act_runner/issues/329" target="_blank" rel="noopener noreferrer" class="external-link"&gt;meiner Suche&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; stieß ich auf &lt;a href="https://gitea.com/gitea/act_runner/src/branch/main/internal/pkg/config/config.example.yaml" target="_blank" rel="noopener noreferrer" class="external-link"&gt;die Möglichkeit, die Konfiguration des Runners&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zu verändern. So könnte ich am Ende vielleicht doch noch ein Volume einbinden, welches ich zwischen der Action und meinem Server über den Daemon auf dem Host teilen kann.&lt;/p&gt;&#10;&lt;p&gt;Wenn das klappt, hätte ich kein zusätzliches Sicherheitsrisiko wie ein öffentlich verfügbarer &lt;code&gt;file transfer container&lt;/code&gt; oder die langen Wartezeiten durch &lt;code&gt;upload-artifact&lt;/code&gt;. Also frisch ans Werk! Es gibt noch &lt;a href="https://gitea.com/gitea/act_runner/issues/407" target="_blank" rel="noopener noreferrer" class="external-link"&gt;weitere Gründe&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, weswegen Volumes in der jeweiligen Action Sinn machen können - es muss also bereits Leute da draußen geben, die das hinbekommen haben.&lt;/p&gt;&#10;&lt;p&gt;Ich versuche also, die Volumes im Action-Skript anzulegen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# workflows/jekyll-build-action.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;container&lt;/span&gt;: &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jvconseil/jekyll-docker:latest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;: &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/tmp/blog_staging:/blog_staging&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/opt/cache:/opt/hostedtoolcache&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Doch irgendwie landen die Artefakte noch immer nicht auf der Platte meines Hosts. Ich vereinfache die Action soweit, dass ich nur noch prüfe, ob die Erzeugung des Volumes auch einen entsprechenden Ordner anlegt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# workflows/jekyll-build-action.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;CHECK_VOLUME ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ls -al /blog_staging&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;und schon schlägt die Aktion wieder fehl:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;gitea-runner-1 | [Deploy Jekyll site/build] [DEBUG] Working directory &amp;#39;/workspace/schallbert/blog&amp;#39;&#10;gitea-runner-1 | [Deploy Jekyll site/build] | ls: /blog_staging: No such file or directory&#10;gitea-runner-1 | [Deploy Jekyll site/build] ❌ Failure - Main --- CHECK_VOLUME ---&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Nach ein paar weiteren, erfolglosen Versuchen (vielleicht hatte ich etwas falsch geschrieben?) und langer Recherche in den Foren fand ich heraus, dass es das &lt;a href="https://gitea.com/gitea/act_runner/issues/329" target="_blank" rel="noopener noreferrer" class="external-link"&gt;valid_volumes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Attribut im &lt;code&gt;act&lt;/code&gt; gibt - taucht das Volume dort nicht auf, so wird es auch nicht eingebunden.&lt;/p&gt;&#10;&lt;p&gt;OK, also schnell der Action hinzugefügt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# jekyll-build-action.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;container&lt;/span&gt;: &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jvconseil/jekyll-docker:latest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;valid_volumes&lt;/span&gt;: &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#39;**&amp;#39;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# This does not work. Also specialized lists indicating the volumes directly won&amp;#39;t work&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;: &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/tmp/blog_staging:/blog_staging&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/opt/cache:/opt/hostedtoolcache&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Aber immer noch nichts. Immerhin kann ich in den Logs mit dem Wissen über &lt;code&gt;valid_volumes&lt;/code&gt; jetzt eine Warnung entdecken:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;/tmp/blog_staging&lt;span style="color:#f92672"&gt;]&lt;/span&gt; is not a valid volume, will be ignored&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;/opt/cache&lt;span style="color:#f92672"&gt;]&lt;/span&gt; is not a valid volume, will be ignored&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Etwas stimmt mit der Übergabe dieser Option nicht. Also recherchiere ich weiter. Es gibt &lt;a href="https://gitea.com/gitea/act_runner/issues/407" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Berichte&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, in denen das Anmelden der Volumes funktioniert. Also erzeuge ich nach &lt;a href="https://gitea.com/gitea/act_runner/src/branch/main#configuration" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; eine Konfigurationsdatei, wo ich das Folgende eintrage:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# runner/config.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# You can specify multiple volumes. If the sequence is empty, no volumes can be mounted.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# valid_volumes:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# - data&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# - /src/*.json&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# If you want to allow any volume, please use the following configuration:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# valid_volumes:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# - &amp;#39;**&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;valid_volumes&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;/tmp/blog_staging&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/opt/hostedtoolcache&amp;#34;&lt;/span&gt;]&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Wichtig:&lt;/em&gt; Die Notation muss als String vorliegen, kommasepariert, und gibt stets die &lt;code&gt;Source&lt;/code&gt; des Volumes an. Also den Teil, der vor dem &lt;code&gt;:&lt;/code&gt; steht.&#10;Doch noch immer erscheint beim Bauen die Fehlermeldung &amp;ldquo;not a valid volume&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;Doch dann fällt mir auf, dass die Config dem &lt;code&gt;act_runner&lt;/code&gt; selbst als Volume zur Verfügung gestellt werden muss - sonst kann der im Container laufende Runner ja gar nicht darauf zugreifen!&lt;/p&gt;&#10;&lt;p&gt;Daher sieht meine &lt;code&gt;docker-compose.yml&lt;/code&gt; für Gitea, Abschnitt &amp;ldquo;runner&amp;rdquo; nun wie folgt aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runner&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/act_runner:latest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;CONFIG_FILE=/config.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_INSTANCE_URL=https://git.schallbert.de&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_RUNNER_NAME=ichlaufe&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_RUNNER_REGISTRATION_TOKEN= &amp;lt;redacted&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./runner/config.yml:/config.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./runner/data:/data&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/opt/hostedtoolcache:/opt/hostedtoolcache&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und endlich:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | ls -al /blog_staging&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; | total &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; | drwxr-xr-x &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; root root &lt;span style="color:#ae81ff"&gt;4096&lt;/span&gt; Dec &lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; 07:32 .&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; | drwxr-xr-x &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; root root &lt;span style="color:#ae81ff"&gt;4096&lt;/span&gt; Dec &lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; 07:46 ..&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ✅ Success - Main --- CHECK_VOLUME ---&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Was für ein Akt. Ich bin so froh, dass jetzt alles durchläuft und ich auf meinem lokalen Hostsystem tatsächlich die gebauten Dateien vorfinde! 🥳&lt;/p&gt;&#10;&lt;h2 id="reverse-proxy-connection-refused"&gt;Noch ein letzter Tipp&lt;/h2&gt;&#10;&lt;p&gt;Wenn ihr so wie ich mit einem Reverse Proxy arbeitet und so komische &lt;code&gt;connection refused&lt;/code&gt; Fehlermeldungen beim Hochfahren des Runners erhaltet wie:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose up&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;+&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Running 2/0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ✔ Container gitea Created 0.0s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ✔ Container gitea-runner-1 Created 0.0s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Attaching to gitea, gitea-runner-1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;info msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Starting runner daemon&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;error msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;fail to invoke Declare&amp;#34;&lt;/span&gt; error&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;unavailable: dial tcp &amp;lt;address&amp;gt;: connect: connection refused&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | Error: unavailable: dial tcp &amp;lt;address&amp;gt; connect: connection refused&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea | Server listening on :: port 22.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 exited with code &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dann ist euer Reverse Proxy entweder falsch eingestellt oder - wie in meinem Falle - nicht hochgefahren.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Die Nutzung des SFTP (Secure File Transfer Protocol), also der Dateitransfer über ssh (secure shell), erhöht die Angriffsfläche meines Systems ein wenig. Der SSH-Dienst ist nämlich dann nicht nur für meine Action, sondern ganz allgemein von außen erreichbar.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>🗙 Gitea Actions - Jekyll Workflow</title><link>https://blog.schallbert.de/gitea-action-runner-native-jekyll/</link><pubDate>Thu, 30 Nov 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/gitea-action-runner-native-jekyll/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-30_ghaction-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: github actions workflow to build Jekyll page"&#10; title="🗙 Gitea Actions - Jekyll Workflow" /&gt;&#10;&lt;p&gt;Nachdem ich in &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/"&gt;diesem Projekt&lt;/a&gt; meinen eigenen, kleinen Server aufgesetzt und mit den entsprechenden Programmen zum Hosten meines Blogs ausgestattet habe, möchte ich ihn nun in die Lage versetzen, die Website automatisch zu bauen.&lt;/p&gt;&#10;&lt;h2 id="gewünschter-ablauf"&gt;Gewünschter Ablauf&lt;/h2&gt;&#10;&lt;p&gt;Ich möchte folgendes erreichen: Sobald Gitea eine &lt;code&gt;push&lt;/code&gt;-Aktion auf dem Blog-Repository verzeichnet, soll ein Action Runner in einem eigenen Docker-Container loslaufen und Ruby, Bundler, Jekyll sowie sämtliche Abhängigkeiten nachladen. Dann soll der Runner meine Quelldateien für den Blog kopieren und dem &lt;code&gt;bundle exec jekyll build&lt;/code&gt; Prozess zur Verfügung stellen, welcher dann die Website baut und schließlich im &lt;code&gt;_site&lt;/code&gt;-Ordner ablegt. Anschließend soll der Runner sich selbst beenden und alle in Anspruch genommenen Ressourcen wieder freigeben.&lt;/p&gt;&#10;&lt;p&gt;An dieser Stelle möchte ich nachkontrollieren können, ob die Website fehlerfrei gebaut wurde und in Zukunft eventuell noch ein paar weitere Prüfungen darüberlaufen lassen - z.B. kaputte Links erkennen, nicht angezeigte Bilder markieren, Rechtschreibprüfung durchführen.&lt;/p&gt;&#10;&lt;p&gt;Die Freigabe sowie die Veröffentlichung soll dann ein anderer Prozess übernehmen, um den ich mich später kümmere.&lt;/p&gt;&#10;&lt;h2 id="ausgangspunkt-github-actions"&gt;Ausgangspunkt: Github Actions&lt;/h2&gt;&#10;&lt;p&gt;Wie üblich wildere ich erst einmal bei anderen Menschen, die ein ähnliches Problem für sich bereits gelöst haben. Ich wurde im &lt;a href="https://github.com/actions/starter-workflows/blob/main/pages/jekyll.yml" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github Actions starter-workflows&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Repository von Github selbst fündig und habe die Workflowdatei für Jekyll mit leichten Modifikationen direkt mal mit meiner Gitea-Instanz ausprobiert:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# jekyll-build-pages.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Sample workflow for building and deploying a Jekyll site&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Deploy Jekyll site&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;run-name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ gitea.actor }} builds Jekyll site&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;on&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;push]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;jobs&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Build job&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;build&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Checkout&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v3&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Setup Ruby&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf&lt;/span&gt; &lt;span style="color:#75715e"&gt;# v1.146.0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ruby-version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;3.1&amp;#39;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Not needed with a .ruby-version file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;bundler-cache&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt; &lt;span style="color:#75715e"&gt;# runs &amp;#39;bundle install&amp;#39; and caches installed gems automatically&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;cache-version&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Increment this number if you need to re-download cached gems&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nachdem ich diesen Workflow in mein Blogverzeichnis unter &lt;code&gt;workflows/jekyll.yml&lt;/code&gt; hochgeladen hatte, lief der Runner tatsächlich direkt los:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-30_rubyfails.jpg" alt="Image: Gitea action runner returns error on ruby install"&gt;&lt;/figure&gt;&#10;&lt;h2 id="probleme-mit-der-ruby-installation"&gt;Probleme mit der Ruby-Installation&lt;/h2&gt;&#10;&lt;p&gt;Schon mal prima. Allerdings wenig erbaulich, dass bereits bei der Installation von Ruby etwas nicht passt. Werfen wir einen Blick in die Logs:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;::error::The current runner (debian-11-x64) was detected as self-hosted because the platform does not match a GitHub-hosted runner image &#10;(or that image is deprecated and no longer supported).&#10;In such a case, you should install Ruby in the $RUNNER_TOOL_CACHE yourself, for example using https://github.com/rbenv/ruby-build&#10;You can take inspiration from this workflow for more details: &#10;https://github.com/ruby/ruby-builder/blob/master/.github/workflows/build.yml&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Na, das ist doch mal eine hilfreiche Fehlermeldung!&lt;/p&gt;&#10;&lt;p&gt;Der Runner läuft um Ressourcen zu sparen standardmäßig im Docker mit einem abgespeckten Image basierend auf Debian namens &lt;code&gt;node:16-bullseye&lt;/code&gt; &lt;a href="https://github.com/nektos/act/blob/master/IMAGES.md" target="_blank" rel="noopener noreferrer" class="external-link"&gt;(Referenz)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Scheinbar ist dieser nicht kompatibel mit dem unter Ubuntu22 betriebenen Github Actions Runner. Ich habe also ein paar Lösungsmöglichkeiten zur Verfügung:&lt;/p&gt;&#10;&lt;h3 id="möglichkeiten-zur-fehlerbehebung"&gt;Möglichkeiten zur Fehlerbehebung&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Den Runner über seine &amp;ldquo;Labels&amp;rdquo; auf Ubuntu22.04 umsatteln.&lt;/li&gt;&#10;&lt;li&gt;Ren Runner nicht im Docker, sondern nativ auf dem Server laufen lassen.&lt;/li&gt;&#10;&lt;li&gt;Wie in der Fehlermeldung vorgeschlagen, Ruby im Cache des Runners vorinstallieren.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/"&gt;Ein Jekyll-Dockerimage verwenden&lt;/a&gt;, welches Jekyll bereits im Gepäck hat und nicht erst die Installation benötigt.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="den-runner-mit-ubuntu22-statt-node16-betreiben"&gt;Den Runner mit Ubuntu22 statt Node16 betreiben&lt;/h3&gt;&#10;&lt;p&gt;Das entsprechende &lt;a href="https://hub.docker.com/_/ubuntu" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dockerimage&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; existiert zwar. Aber es kostet aber einen Haufen Speicherplatz auf meinem Server, benötigt durch seinen größeren Funktionsumfang mehr RAM und braucht vor allem länger, bis es hochgefahren ist.&lt;/p&gt;&#10;&lt;p&gt;Und Zeit ist bei Runnern wertvoll, schließlich will ich ja möglichst schnell wissen, ob das Bauen geklappt hat.&lt;/p&gt;&#10;&lt;h3 id="den-runner-direkt-auf-der-hostmaschine-ubuntu22-laufen-lassen"&gt;Den Runner direkt auf der Hostmaschine (Ubuntu22) laufen lassen&lt;/h3&gt;&#10;&lt;p&gt;Ist sicherheitstechnisch nicht so sehr zu empfehlen (Bei &lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches" target="_blank" rel="noopener noreferrer" class="external-link"&gt;einem ungeschützten Branch&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; würde der Runner von Dritten per &lt;code&gt;git push --force&lt;/code&gt; hochgeschobenen Schadcode unbesehen auf meiner Hostmaschine ausführen) und zudem ginge die Portabilität flöten, weil ich das Zielsystem des Runners dann fest verdrahte.&lt;/p&gt;&#10;&lt;h3 id="gitea-act_runner-direkt-auf-dem-host-laufen-lassen"&gt;Gitea act_runner direkt auf dem Host laufen lassen&lt;/h3&gt;&#10;&lt;p&gt;Für den zweiten Ansatz habe ich mir per &lt;a href="https://docs.gitea.com/1.23/usage/actions/act-runner/#configuration" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; eine Konfiguration für den &lt;code&gt;runner&lt;/code&gt; angelegt und darin das bestehende label-mapping so geändert, dass nicht mehr in Richtung Docker, sondern auf dem Ubuntu des Servers selbst gebaut wird. Dafür genügt es in meinem Fall, das Label &lt;code&gt;ubuntu-latest:host&lt;/code&gt; statt &lt;code&gt;ubuntu-latest&lt;/code&gt; in die Konfiguration zu schreiben. Leider erhielt ich hier Fehlermeldungen bei den Zugriffsrechten des Runners, welcher aus Docker heraus auf dem Hostsystem laufen müsste.&lt;/p&gt;&#10;&lt;p&gt;Nicht so schlimm, ich wollte diese Lösung wegen der oben beschriebenen möglichen Probleme sowieso nicht dauerhaft verwenden. Also investiere ich hier keine weitere Zeit.&lt;/p&gt;&#10;&lt;h3 id="ruby-im-cache-vorinstallieren"&gt;Ruby im Cache vorinstallieren&lt;/h3&gt;&#10;&lt;p&gt;Wie von der Fehlermeldung empfohlen habe ich &lt;a href="https://about.gitea.com/resources/tutorials/enable-gitea-actions-cache-to-accelerate-cicd" target="_blank" rel="noopener noreferrer" class="external-link"&gt;dieser Anleitung folgend&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; in der &lt;code&gt;docker-compose.yml&lt;/code&gt; von Gitea ein Volume angelegt für den Tool Cache des runners und das Zielverzeichnis in der Umgebungsvariable &lt;code&gt;RUNNER_TOOL_CACHE&lt;/code&gt; abgelegt.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;runner&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/act_runner:nightly&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_INSTANCE_URL=&amp;lt;redacted&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_RUNNER_NAME=&amp;lt;redacted&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;RUNNER_TOOL_CACHE=/opt/hostedtoolcache&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_RUNNER_REGISTRATION_TOKEN= &amp;lt;redacted&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./runner/data:/data&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/opt/hostedtoolcache:/opt/hostedtoolcache&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hiermit wird dem in Docker laufenden Runner der Ordner &lt;code&gt;/opt/hostedtoolcache&lt;/code&gt; im Dateisystem des Hosts unter dem Namen &lt;code&gt;/opt/hostedtoolcache&lt;/code&gt; zur Verfügung gestellt.&#10;Nun probiere ich mal, Ruby direkt dort hinein zu installieren.&lt;/p&gt;&#10;&lt;p&gt;Also lade ich das &lt;a href="https://github.com/rbenv/ruby-build" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;ruby-build&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; repository herunter (&lt;code&gt;git clone&lt;/code&gt;) und lasse das Installerscript mit &lt;code&gt;./ruby-build/install.sh&lt;/code&gt; durchlaufen.&#10;Danach führe ich den Build mit dem in der Fehlermeldung angegebenen Pfad aus: &lt;code&gt;ruby-build 3.1.4 /opt/hostedtoolcache/Ruby/3.1.4/x64&lt;/code&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;==&amp;gt; Downloading openssl-3.1.4.tar.gz...&#10; % Total % Received % Xferd Average Speed Time Time Time Current&#10; Dload Upload Total Spent Left Speed&#10;100 14.8M 100 14.8M 0 0 81.3M 0 --:--:-- --:--:-- --:--:-- 81.1M&#10;==&amp;gt; Installing openssl-3.1.4...&#10;&#10;BUILD FAILED (Ubuntu 22.04 on x86_64 using ruby-build 20231114)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="probleme-mit-prebuilt-ruby"&gt;Probleme mit prebuilt-Ruby&lt;/h3&gt;&#10;&lt;p&gt;Das Log zeigt das Problem:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;[...] No C compiler found, please specify one with the environment variable CC [...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Also tippe ich &lt;code&gt;apt install build-essential&lt;/code&gt; und stelle dann mit &lt;code&gt;which gcc&lt;/code&gt; fest, dass er nun unter &lt;code&gt;/usr/bin/gcc&lt;/code&gt; installiert ist. Auf ein Neues!&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;crypto/comp/c_zlib.c:36:11: fatal error: zlib.h: No such file or directory&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Dann schnell &lt;code&gt;apt install libz-dev&lt;/code&gt; eingeben und erneut probieren. Dieses mal läuft er recht lange, dann:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;*** Following extensions are not compiled:&#10;openssl:&#10; Could not be configured. It will not be installed.&#10; /tmp/ruby-build.20231205114312.9174.0gmhNf/ruby-3.1.4/ext/openssl/extconf.rb:100: OpenSSL library could not be found. You might Check ext/openssl/mkmf.log for more details.&#10;readline:&#10; Could not be configured. It will not be installed.&#10; /tmp/ruby-build.20231205114312.9174.0gmhNf/ruby-3.1.4/ext/readline/extconf.rb:62: Neither readline nor libedit was found&#10; Check ext/readline/mkmf.log for more details.&#10;*** Fix the problems, then remove these directories and try again if you want.&#10;[...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;OK, nun ist es openssl. Fix nachinstallieren mit &lt;code&gt;apt install libssl-dev&lt;/code&gt; sowie &lt;code&gt;apt install libreadline-dev&lt;/code&gt; und endlich 🎉:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;Installed ruby-3.1.4 to /opt/hostedtoolcache/Ruby/3.1.4/x64&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="noch-immer-fehler-beim-bauen-mit-dem-github-acitons-script"&gt;Noch immer Fehler beim Bauen mit dem Github-Acitons script&lt;/h2&gt;&#10;&lt;p&gt;Merkwürdig, das Log spuckt mir immer noch denselben Fehler aus:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;💬 ::debug::isExplicit: 3.1.4&#10;&#10;gitea-runner-1 | [Deploy Jekyll site/build] 💬 ::debug::checking cache: /opt/hostedtoolcache/Ruby/3.1.4/x64&#10;gitea-runner-1 | [Deploy Jekyll site/build] | ::debug::checking cache: /opt/hostedtoolcache/Ruby/3.1.4/x64&#10;gitea-runner-1 | [Deploy Jekyll site/build] 💬 ::debug::not found&#10;gitea-runner-1 | [Deploy Jekyll site/build] | ::debug::not found&#10;gitea-runner-1 | [Deploy Jekyll site/build] ❗ ::error::The current runner (debian-11-x64) was detected as self-hosted&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Das geht auf folgende Abfage in der ausführenden &lt;a href="https://github.com/ruby/setup-ruby/blob/master/ruby-builder.js#L97" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Action&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zurück:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// setup-ruby/ruby-builder.js&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;common&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;shouldUseToolCache&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;engine&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;version&lt;/span&gt;)) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;inToolCache&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;common&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;toolCacheFind&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;engine&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;version&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;inToolCache&lt;/span&gt;) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;rubyPrefix&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;inToolCache&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;toolCacheRubyPrefix&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;common&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getToolCacheRubyPrefix&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;platform&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;engine&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;version&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;common&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;isSelfHostedRunner&lt;/span&gt;()) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rubyBuildDefinition&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;engine&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;ruby&amp;#39;&lt;/span&gt; &lt;span style="color:#f92672"&gt;?&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;version&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;engine&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;-&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;version&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;core&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;( [...] )&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Ursachen für fehlenden `toolcache`&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-01-06T00:00:00Z"&gt;&#10; 06. Jan. 2024&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Es es gibt zwei weitere Möglichkeiten dafür, dass der &lt;code&gt;toolcache&lt;/code&gt; nicht gefunden wird: Das Volume wird dem Runner zur Verfügung gestellt, aber nicht in die Action weitergereicht (siehe diesen &lt;a href="https://blog.schallbert.de/build-deploy-hugo-with-actions-docker-caddy/#docker-shared-volumes"&gt;Beitrag zum Thema shared Volumes&lt;/a&gt;). Somit sind die Daten am eigentlichen Ziel nicht verfügbar. Oder der &lt;code&gt;toolcache&lt;/code&gt;-Pfad ist nicht in der Konfigurationsdatei des runners unter dem Eintrag &lt;code&gt;valid_volumes&lt;/code&gt; enthalten. Eine &lt;a href="https://gitea.com/gitea/act/pulls/60" target="_blank" rel="noopener noreferrer" class="external-link"&gt;pull request Kommunikation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; lässt mich jedoch vermuten, dass dies auch automatisch geschehen kann.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h3 id="docker-volume-überprüfen"&gt;Docker volume überprüfen&lt;/h3&gt;&#10;&lt;p&gt;Mist. Prüfen wir schnell mal, ob Ruby in dem Volume auch wirklich verfügbar ist. Dafür suchen wir mit &lt;code&gt;docker ps&lt;/code&gt; die Container ID des runners heraus und geben sie bei folgendem Kommando ein:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker exec -it &amp;lt;containerID&amp;gt; bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dann navigiere ich in den gesuchten Ordner &lt;code&gt;cd /opt/hostedtoolcache&lt;/code&gt;, liste dort den Inhalt mit &lt;code&gt;ls&lt;/code&gt; und siehe da: &lt;code&gt;Ruby&lt;/code&gt;. Es liegt also alles vor, nur wird es nicht gefunden.&lt;/p&gt;&#10;&lt;h2 id="misserfolg-anerkennen"&gt;Misserfolg anerkennen&lt;/h2&gt;&#10;&lt;p&gt;So komme ich hier also nicht weiter 😖. Daher öffne ich mein Problem für die Community unter dem Titel &lt;a href="https://gitea.com/gitea/act_runner/issues/441" target="_blank" rel="noopener noreferrer" class="external-link"&gt;act_runner cannot find hostedtoolcache&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und probiere&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/"&gt;einen anderen Ansatz&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="problemursache"&gt;Problemursache&lt;/h3&gt;&#10;&lt;p&gt;Der Support der Action &lt;code&gt;setup-ruby&lt;/code&gt; für selbsgehostete CI-Pipelines wie meinen &lt;code&gt;act_runner&lt;/code&gt; wurde &lt;a href="https://github.com/ruby/setup-ruby/issues/475#issuecomment-1455099634" target="_blank" rel="noopener noreferrer" class="external-link"&gt;wohl eingestellt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Die Dokumentation mit den Hinweisen zur Fehlerbehebung bei Verwendung selbstgehosteter Runner allerdings zum Zeitpunkt der Verwendung durch meine Wenigkeit war aber noch nicht auf Stand gebracht worden.&lt;/p&gt;&#10;</description></item><item><title>Mehrsprachige Webseiten - l10n</title><link>https://blog.schallbert.de/jekyll-polyglot-language-support/</link><pubDate>Tue, 14 Nov 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/jekyll-polyglot-language-support/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-14_translations_polyglot_flags-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Flags that can be clicked on to change display language"&#10; title="Mehrsprachige Webseiten - l10n" /&gt;&#10;&lt;p&gt;Die Bereitstellung einer Webseite in mehreren Sprachen wird &amp;ldquo;internationalization&amp;rdquo; - kurz &lt;a href="https://en.wikipedia.org/wiki/Internationalization_and_localization" target="_blank" rel="noopener noreferrer" class="external-link"&gt;i16n&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; beziehungsweise &amp;ldquo;localization&amp;rdquo; &lt;code&gt;l10n&lt;/code&gt; genannt. Sucht man nach diesen Begriffen im Netz, findet man eine Menge tollen Materials zum Thema. Die Zahlen in den Kurzformen dieser Begriffe geben die Anzahl Buchstaben in der Wortmitte an.&lt;/p&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Ich bin schon vor langer Zeit gefragt worden, ob ich nicht mal eine &lt;a href="https://hobbyline.info/forum/index.php?thread/603-blog-artikel-zur-portalfr%C3%A4se/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;deutsche Version dieser Webseite&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; anfertigen wolle. Bis dato hielt ich das für &lt;a href="https://hobbyline.info/forum/index.php?thread/603-blog-artikel-zur-portalfr%C3%A4se/&amp;amp;postID=7619#post7619" target="_blank" rel="noopener noreferrer" class="external-link"&gt;zu viel Arbeit&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Dies änderte sich erst vor gut einem halben Jahr, als ich dieselbe Anfrage verbal von mehreren Leuten unabhängig voneinander in kurzer Zeit erhielt.&lt;/p&gt;&#10;&lt;p&gt;Also investierte ich einen Haufen freier Abende in die Fleißarbeit, rund 80 Artikel auf Deutsch zu übertragen. Doch ich war noch immer nicht fertig.&lt;/p&gt;&#10;&lt;h2 id="jekyll-plugin"&gt;Jekyll plugin&lt;/h2&gt;&#10;&lt;p&gt;Ich sah mich nach Möglichkeiten um, meinen Internetauftritt mit Jekyll mehrsprachig zu gestalten. Dabei fand ich unter Anderem &lt;a href="https://demondehellis.github.io/en/posts/how-to-make-jekyll-blog-multilingual/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;demondehellis&amp;rsquo; plugin-freie Lösung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://polyglot.untra.io/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;untras polyglot plugin&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://github.com/kurtsson/jekyll-multiple-languages-plugin" target="_blank" rel="noopener noreferrer" class="external-link"&gt;kurtssons jekyll-multiple-languages-plugin&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und noch ein paar weitere Möglichkeiten, diesen Webauftritt in mehreren Sprachen anzubieten.&lt;/p&gt;&#10;&lt;p&gt;Nun musste ich mich für eine entscheiden. Die plugin-freie Lösung fiel recht schnell heraus, da ich &amp;ldquo;gleiche&amp;rdquo; Posts verschiedener Sprache gleich benennen wollte und damit Interlinks zwischen Beiträgen unabhängig von der verwendeten Sprache bestehen bleiben sollten - irgendwas muss also dafür sorgen, dass sich die Sprachen nicht wild mischen.&lt;/p&gt;&#10;&lt;p&gt;Blieben die Plugins übrig. Kurz gesagt stieß ich zufällig auf &lt;a href="https://leo3418.github.io/collections/multilingual-jekyll-site/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;leo3418s&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; hervorragende Artikelserie für die Implementierung von Polyglot - und so entschied ich mich für ebendieses Plugin.&lt;/p&gt;&#10;&lt;h3 id="polyglot-installation"&gt;Polyglot: Installation&lt;/h3&gt;&#10;&lt;p&gt;Die Installation von Polyglot war einfach, denn sowohl die &lt;a href="https://github.com/untra/polyglot" target="_blank" rel="noopener noreferrer" class="external-link"&gt;im Repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bereitgestellte &lt;code&gt;readme.md&lt;/code&gt; als auch der von leo3418 geschriebene &lt;a href="https://leo3418.github.io/collections/multilingual-jekyll-site/set-up-polyglot.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Blogartikel&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; beschäftigen sich ausführlich mit den ersten Schritten. In der Tat fand ich die Dokumentation so gut, dass ich darauf verweisen möchte und hier gar nicht weiter darüber berichten werde.&lt;/p&gt;&#10;&lt;h3 id="restructuring-the-website"&gt;Umstrukturierung der Website&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-14_folderstructure.jpg" alt="image: updated folder structure for this site"&gt;&lt;/figure&gt;&#10;Bei Polyglot gibt es zwei Möglichkeiten, die Ordnerstruktur für die Website auf mehrsprachig umzustellen:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Man benutzt das Sprachkürzel im Artikelnamen und ermöglicht so eine Zuordnung. Dieser Artikel hier würde dann z.B. heißen: &lt;code&gt;2023-11-14-jekyll-polyglot-language-support-de.md&lt;/code&gt; und analog in jeder weiteren Sprache mit einem anderen Sprachkürzel&lt;/li&gt;&#10;&lt;li&gt;Man erstellt Ordner mit dem Sprachkürzel, z.b. &lt;code&gt;/de&lt;/code&gt;, behält aber identische Dateinamen für verschiedene Sprachen.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Ich habe mich für letzteres entschieden, um der Umbenennung meiner schon vorhandenen Artikel zu entgehen. Die Sprache wird von Polyglot auch bei Verwendung von Interlinks auf der Webseite beibehalten. Klickt man z.B. auf den Artikel &lt;a href="https://blog.schallbert.de/milling-small-parts/"&gt;https://schallbert.de/milling-small-parts/&lt;/a&gt;, so wird Polyglot ausgehend von der Sprache des aktuellen Artikels auch den neuen Artikel in dieser Sprache aufrufen. Einzig bei Überschriften von Abschnitten wird es schwierig: Die sind nämlich übersetzt. Also wird der Link auf die englische Version der Seite für die Überschrift &lt;a href="https://blog.schallbert.de/jekyll-polyglot-language-support/#restructuring-the-website"&gt;#Umstrukturierung-der-Website&lt;/a&gt; in einem 404-Fehler enden.&lt;/p&gt;&#10;&lt;p&gt;Daher muss man für die Überschriften den kompletten Artikelpfad inkl. der Lokalisierung angeben. In diesem Beispiel wäre folglich &lt;a href="https://blog.schallbert.de/jekyll-polyglot-language-support/#restructuring-the-website"&gt;/posts/en/jekyll-polyglot-language-support/#restructuring-the-website&lt;/a&gt; der korrekte Interlink.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Permalink-IDs&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-05-11T00:00:00Z"&gt;&#10; 11. Mai 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Alternativ kann man der Überschrift auch eine sprachunabhängige ID geben, sodass auch hier die Anchor-Links auf die Überschriften identisch bleiben. Beispiel: &lt;code&gt;## Umstrukturierung der Website {# website-restructuring}&lt;/code&gt;, Aufruf im Link per &lt;code&gt;#website-restucturing&lt;/code&gt;.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h3 id="übersetzung-von-seitentitel-navigation-kopf--und-fußzeile"&gt;Übersetzung von Seitentitel, Navigation, Kopf- und Fußzeile&lt;/h3&gt;&#10;&lt;p&gt;Hier wird es ein wenig komplizierter. Die &lt;code&gt;_config.yml&lt;/code&gt; von Jekyll lässt ohne Weiteres nicht zu, dass mehrere Varianten von ihr existieren. Man benötigt für sprachsensitive Parameter wie &lt;code&gt;site.title&lt;/code&gt; oder &lt;code&gt;site.description&lt;/code&gt; in der config daher die Möglichkeit, die Standardwerte zu überschreiben. Meine Methode das zu lösen war, im &lt;code&gt;_data&lt;/code&gt;-Ordner eine Datei mit Namen &lt;code&gt;l10n.yml&lt;/code&gt; anzulegen, in der dann diese Parameter abgelegt sind. Um sie dann auch korrekt anzuzeigen, sind die &lt;code&gt;html&lt;/code&gt;-Dateien im &lt;code&gt;_includes&lt;/code&gt;-Ordner so anzupassen, dass Parameter dieser Datei statt der config eingelesen und durch die Templatesprache Liquid dann in html umgesetzt werden. Beispiel: &lt;code&gt;{{ site.data.ui-text[site.data.l10n.locale].follow_label | remove: &amp;quot;:&amp;quot; | default: &amp;quot;Follow&amp;quot; }}&lt;/code&gt;. Hier wird dann wie im Bild gezeigt dann in der deutschen Version &lt;strong&gt;FOLGEN:&lt;/strong&gt; angezeigt und in der englischen &lt;strong&gt;FOLLOW:&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;h3 id="sprachauswahl"&gt;Sprachauswahl&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-14_translations_polyglot_flags.jpg" alt="image: footer section of this website"&gt;&lt;/figure&gt;&#10;Natürlich möchte ich die Möglichkeit geben, die Sprache der Website selbst zu wählen. Ich habe mich hier für das Sprachkürzel sowie die entsprechende Flagge entschieden, welche im unteren Bereich einer jeden Seite eingeblendet werden. Klickt man hier drauf, so wird die Seite in der entsprechenden Sprache neu geladen. Dafür war es notwendig, einen &amp;ldquo;language-switcher&amp;rdquo; in Liquid zu schreiben.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// _includes/language-switcher.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; raw &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; lang in site.languages &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; assign url_parts &lt;span style="color:#f92672"&gt;=&lt;/span&gt; page.url &lt;span style="color:#f92672"&gt;|&lt;/span&gt; split: &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; assign lang_code &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lang &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; assign collection_name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; url_parts[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; assign post_url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; url_parts &lt;span style="color:#f92672"&gt;|&lt;/span&gt; last &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; lang &lt;span style="color:#f92672"&gt;==&lt;/span&gt; site.default_lang &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; collection_name &lt;span style="color:#f92672"&gt;==&lt;/span&gt; post_url &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; assign lang_url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: post_url &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; assign lang_url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: collection_name &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: post_url &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; endif &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; url_parts.size &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; assign lang_url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: lang_code &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; collection_name &lt;span style="color:#f92672"&gt;==&lt;/span&gt; post_url &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; assign lang_url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lang_url &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: post_url &lt;span style="color:#f92672"&gt;%&lt;/span&gt;} &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; assign lang_url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lang_url &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: collection_name &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: lang_url &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: post_url &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; endif &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#f92672"&gt;%&lt;/span&gt; endif &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;a href&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34; {{ lang_url }}&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;{{ site.data[lang].l10n.lang_name }}&lt;span style="color:#f92672"&gt;&amp;lt;/&lt;/span&gt;a&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; endfor &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; endraw &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hierfür wird die aktuelle &lt;code&gt;url&lt;/code&gt; am Slash in Einzelteile zerlegt. Anschließend der letzte Teil der url als Artikelname &lt;code&gt;post_url&lt;/code&gt; gesetzt und angenommen, dass der erste Teil der URL der collection entspricht, z.B. &lt;code&gt;posts-hardware&lt;/code&gt;. Die anschließende Logik setzt die Sprachkürzel je nach Bedarf in die url ein, sodass je nach Verwendung einer collection oder ohne die entsprechende Zielurl korrekt erzeugt wird.&lt;/p&gt;&#10;&lt;h2 id="veröffentlichung"&gt;Veröffentlichung&lt;/h2&gt;&#10;&lt;p&gt;Ich war echt stolz auf diesen Umbau. Ich finde, dass sich alles gut einfügt und ich mit der Zweisprachigkeit meinen Nutzerkreis gut erweitern kann. Der Nachteil für mich ist die zusätzliche Übersetzungsarbeit, die von nun an bei jedem Artikel auf mich zukommt.&lt;/p&gt;&#10;&lt;p&gt;Als ich diese Seite das erste mal auf die Github Page hochlud, merkte ich, dass das so überhaupt nicht funktioniert. Github Pages unterstützt Polyglot nämlich nicht, sodass plötzlich alle Artikel unabhängig von ihrer Sprache auf einmal auf meiner Website erscheinen.&lt;/p&gt;&#10;&lt;p&gt;Nun blieben mir nur zwei Möglichkeiten: &lt;a href="https://blog.schallbert.de/struggling-github-actions/"&gt;Eigene Github Actions&lt;/a&gt; für das Deployment aufzusetzen - oder ein Umzug auf einen eigenen Server. Ich habe mich für Letzteres entschieden, da die Github-Server für diese Seite meines Wissens nach in UK stehen und und ich mit einem Server in Deutschland lieber auf der sicheren Seite wäre.&lt;/p&gt;&#10;&lt;p&gt;Der eigentliche Umbau ist aber deutlich zu groß, um ihn in diesem Post abzuhandeln - dafür gibt es ein &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/"&gt;eigenes Softwareprojekt&lt;/a&gt;.&lt;/p&gt;&#10;</description></item><item><title>Github Actions: Fehler beheben</title><link>https://blog.schallbert.de/struggling-github-actions/</link><pubDate>Tue, 14 Feb 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/struggling-github-actions/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-02-14_github_actions-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Github Actions Workflow"&#10; title="Github Actions: Fehler beheben" /&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Warum ich einen Beitrag zu Github Actions schreibe, obwohl es im Internet zahllose Tutorials und Blogeinträge zum Thema gibt? Weil ich für mein letztes Software-Projekt &lt;a href="https://blog.schallbert.de/projects/qr-codengrave/"&gt;QR-codengrave&lt;/a&gt; eine Build Pipeline aufsetzen wollte und sich dies schwieriger gestaltete als angenommen. Sollte ich das an irgend einem Punkt in der Zukunft wiederholen müssen und erinnere mich nicht, wie ich damals in der IDE gebaut, getestet und veröffentlicht habe, kann die Pipeline weiterhelfen.&lt;/p&gt;&#10;&lt;p&gt;Ebenso, wenn sich meine IDE ändert oder meine Virtuelle Umgebung defekt ist, die &lt;code&gt;launch.json&lt;/code&gt; fehlt oder irgend etwas inkompatibel mit dem von mir verwendeten Compiler/Interpreter wird. Dennoch will ich in der Lage sein, Releases zu veröffentlichen, Bugfixes zu erstellen und Tests zu fahren.&lt;/p&gt;&#10;&lt;p&gt;Obwohl das nicht das übliche Argument ist, eine CI/CD Pipeline aufzubauen - normalerweise wird da zuerst die einfachere Zusammenarbeit mehrerer Menschen an einem gemeinsamen Programm(teil) genannt - ist der Anreiz für mich stark genug, mir mal Githubs Automation dafür, bekannt unter dem Namen &lt;a href="https://docs.github.com/en/actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github Actions&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, auszuprobieren.&lt;/p&gt;&#10;&lt;p&gt;Meine Software ist zu unbedeutend um &amp;ldquo;Nightlies&amp;rdquo; zu bauen oder agil entwickelt zu werden, und daher werde ich die Pipeline auf &amp;ldquo;build&amp;rdquo;, &amp;ldquo;lint/static code checks&amp;rdquo;, &amp;ldquo;testing&amp;rdquo; und &amp;ldquo;deployment&amp;rdquo; beschränken und dies auch nur dann automatisch triggern, wenn es Änderungen auf dem Produktionsbranch gibt.&lt;/p&gt;&#10;&lt;h3 id="alles-klar-aber-warum-ein-extra-blogeintrag"&gt;Alles klar, aber warum ein extra Blogeintrag?&lt;/h3&gt;&#10;&lt;p&gt;Weil es mich so viel Zeit gekostet hat, alles richtig aufzusetzen. Ich habe dafür stundenlang Fehler suchen, oft fluchen und wütend Spaziergänge machen müssen, bevor ich meine in der IDE bereits getestete und lauffähige Software auch mit Github vereinen zu können.&lt;/p&gt;&#10;&lt;p&gt;Wäre ich klüger gewesen, hätte ich gleich zu Anfang nach Tools dafür recherchiert &lt;a href="https://github.com/nektos/act" target="_blank" rel="noopener noreferrer" class="external-link"&gt;wie dieses hier&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, sodass Github Actions auch lokal ausgeführt werden kann. Dies hätte mir die ständigen Pushes und Wartezeiten auf den nächsten, immer noch fehlerhaften Build ersparen können.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-02-14_gh_actions_struggle.jpg" alt="Image: Github Action failed runs"&gt;&lt;/figure&gt;&#10;&lt;h2 id="das-script"&gt;Das Script&lt;/h2&gt;&#10;&lt;p&gt;Github Actions benutzt &lt;code&gt;YAML&lt;/code&gt; um Befehle der Nutzer zu interpretieren. Die Verwendung ist sehr gut &lt;a href="https://docs.github.com/en/actions/learn-github-actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;dokumentiert&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und stellt einige &lt;a href="https://github.com/marketplace?type=actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;plug-and-play&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Beispiele bereit, die in vielen Szenarien und für die meisten Programmiersprachen direkt passen.&lt;/p&gt;&#10;&lt;p&gt;Die sogenannte &amp;ldquo;Workflowdatei&amp;rdquo; sieht so aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-YAML" data-lang="YAML"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# This workflow will install Python dependencies, run tests and lint with a single version of Python&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;python_integrate&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;on&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;push&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;branches&lt;/span&gt;: [ &lt;span style="color:#e6db74"&gt;&amp;#34;master&amp;#34;&lt;/span&gt; ]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;pull_request&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;branches&lt;/span&gt;: [ &lt;span style="color:#e6db74"&gt;&amp;#34;master&amp;#34;&lt;/span&gt; ]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;permissions&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;contents&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;read&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;jobs&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;build&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v3&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Set up Python 3.10&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/setup-python@v3&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;python-version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;3.10&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Install dependencies&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; python -m pip install --upgrade pip&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; pip install flake8 pytest&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; pip install qrcodegen # Dependency install of qrcodegen&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; if [ -f requirements.txt ]; then pip install -r requirements.txt; fi&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Lint with flake8&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # stop the build if there are Python syntax errors or undefined names&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Test with pytest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;GabrielBB/xvfb-action@v1 &lt;/span&gt; &lt;span style="color:#75715e"&gt;# Diverts tkinter GUI to a virtual frame buffer (VFB)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; pytest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dieses sieht nicht einmal groß anders aus als die Vorlage mit der ich gestartet bin. Die einzigen Anpassungen waren der nun vorhandene &lt;code&gt;pytest&lt;/code&gt; Aufruf sowie das Hinzufügen eines virtuellen Bildpuffers um Darstellungsprobleme meiner GUI zu lösen. Meine Tests schlugen nämlich fehl sobald die GUI gestartet wurde, da auf den Github Servern gar kein Display zur Verfügung steht.&lt;/p&gt;&#10;&lt;h2 id="github-action-runner-fehlermeldungen"&gt;Github Action runner Fehlermeldungen&lt;/h2&gt;&#10;&lt;p&gt;Die folgende Liste enthält die Fehler (und zugehörige Lösungen) mit denen ich konfrontiert wurde, bevor ich eine stabile CI aufsetzen konnte - erst nach fast 50 Durchläufen.&lt;/p&gt;&#10;&lt;h3 id="ordner-nicht-vorhanden"&gt;Ordner nicht vorhanden?&lt;/h3&gt;&#10;&lt;p&gt;&lt;em&gt;Output&lt;/em&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;opt&lt;span style="color:#f92672"&gt;/&lt;/span&gt;hostedtoolcache&lt;span style="color:#f92672"&gt;/&lt;/span&gt;Python&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3.10.9&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;x64&lt;span style="color:#f92672"&gt;/&lt;/span&gt;lib&lt;span style="color:#f92672"&gt;/&lt;/span&gt;python3&lt;span style="color:#ae81ff"&gt;.10&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;importlib&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;__init__&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;py:&lt;span style="color:#ae81ff"&gt;126&lt;/span&gt;: &lt;span style="color:#f92672"&gt;in&lt;/span&gt; import_module&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; _bootstrap&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_gcd_import(name[level:], package, level)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test&lt;span style="color:#f92672"&gt;/&lt;/span&gt;test_machinify_vector&lt;span style="color:#f92672"&gt;.&lt;/span&gt;py:&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;: &lt;span style="color:#f92672"&gt;in&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;module&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;from&lt;/span&gt; bin.platform.machinify_vector &lt;span style="color:#f92672"&gt;import&lt;/span&gt; MachinifyVector, Tool, EngraveParams&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;E &lt;span style="color:#a6e22e"&gt;ModuleNotFoundError&lt;/span&gt;: No module named &lt;span style="color:#e6db74"&gt;&amp;#39;bin&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Hintergrund&lt;/em&gt;:&#10;Ich habe meine Quelldateien unter &lt;code&gt;/bin&lt;/code&gt; abgelegt und meiner lokalen Instanz von Pyinstaller (das Werkzeug, was ich auswählte, um aus den Skripten eine ausführbare Datei zu erzeugen) zur Verfügung gestellt. Beim Upload konnte Github Actions die Pfade nicht auflösen und zeigte diesen Fehler an.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Lösung:&lt;/em&gt;&#10;Eine leere Datei mit Namen &lt;code&gt;__init__.py&lt;/code&gt; im &lt;code&gt;/bin&lt;/code&gt; Ordner ablegen. Dies zeigt alle im Ordner befindlichen Dateien als Package an, die so für Github Actions verfügbar werden.&#10;Die IDE hatte dieses Problem nicht, weil sie alle im Projektordner befindlichen Dateien automatisch indiziert.&lt;/p&gt;&#10;&lt;h3 id="yaml-syntaxfehler"&gt;YAML Syntaxfehler&lt;/h3&gt;&#10;&lt;p&gt;Diese Fehler entstanden, weil ich versucht hatte, mehrere Workflows in eine Datei zu schreiben. Github Actions scheint nur einen Workflow pro Datei zu akzeptieren.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-02-14_gh_actions_syntax.jpg" alt="Image: Github Action syntax error"&gt;&lt;/figure&gt;&#10;&lt;h3 id="fehler-pfad-nicht-gefunden"&gt;Fehler: Pfad-nicht-gefunden&lt;/h3&gt;&#10;&lt;p&gt;Ich hatte lange Probleme mit solchen Fehlern bedingt durch meine Ordnerstruktur:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-YAML" data-lang="YAML"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#ae81ff"&gt;assets &lt;/span&gt; &lt;span style="color:#75715e"&gt;# images, persistence file, etc.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#ae81ff"&gt;src &lt;/span&gt; &lt;span style="color:#75715e"&gt;# source files&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#ae81ff"&gt;test &lt;/span&gt; &lt;span style="color:#75715e"&gt;# pytest files for unit and integration testing&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#ae81ff"&gt;dist &lt;/span&gt; &lt;span style="color:#75715e"&gt;# build artifacts&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#ae81ff"&gt;build &lt;/span&gt; &lt;span style="color:#75715e"&gt;# build process files&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich fand die echt schön aufgeräumt. Aber meine lokale IDE, PyInstaller und Github Actions bzw. dessen worker für Pyinstaller waren sich nicht einig, wie sow etwas aussehen soll. Immer würde sich eine der beteiligten Parteien beschweren, dass ein Pfad nicht existiere oder die Datei soundso nicht gelesen werden könne. Daher habe ich am Ende die Verwendung relativer Pfade verworfen und stattdessen pythons &lt;code&gt;importlib_resources&lt;/code&gt; verwendet.&lt;/p&gt;&#10;&lt;p&gt;Aber auch hier gab es Probleme mit Pyinstaller auf Github.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-02-14_gh_actions_paths.jpg" alt="Image: Github Action relative path error"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Letztendlich konnte ich das Problem erst lösen, als ich den Ordner für &lt;code&gt;assets&lt;/code&gt; in &lt;code&gt;src&lt;/code&gt; integrierte. Auf diese weise kann der &lt;code&gt;./&lt;/code&gt; Operator nicht falsch verstanden werden. Obwohl ich dieses Konstrukt nicht so schön finde, war ich immer weniger bereit, noch mehr Zeit dort hinein zu buttern. (Wenn Sie wissen, wie sich das Pfadhandling sowohl lokal als auch remote mit Eleganz lösen lässt, freue ich mich sehr über Ihren Beitrag auf meiner &lt;a href="https://github.com/Schallbert/schallbert.github.io/discussions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dissussionsseite&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;).&lt;/p&gt;&#10;&lt;h3 id="tkinter-headless"&gt;Tkinter headless&lt;/h3&gt;&#10;&lt;p&gt;Spät im Entwicklungsprozess habe ich meinen Unit Tests ein paar Integrationstests zur Seite gestellt. So wollte ich sicherstellen, dass Popups die erforderlichen Callbacks an Main machen würden und umgekehrt Daten ausgetauscht werden können, sodass z.B. eine Werkzeugliste an das Konfigfenster übergeben werden kann.&lt;/p&gt;&#10;&lt;p&gt;Diese Tests liefen lokal zwar, aber mit einem Nachteil: Wenn ich einen Fehler oder eine Warnung erzeugte, ließ sich das &lt;code&gt;messageBox&lt;/code&gt;-Popup nicht automatisiert schließen. Es wollte unbedingt vom Nutzer selbst per Mausklick geschlossen werden. Dies ließ sich auch nicht durch einen Overlay oder Invoke der Click-Aktion umgehen.&lt;/p&gt;&#10;&lt;p&gt;Was also in der IDE nervig war, würde auf dem Server in der Automation zu Testabbrüchen führen. Also habe ich die bittere Pille geschluckt und eine Wrapperklasse um tkinters &lt;code&gt;messageBox&lt;/code&gt; geschrieben, sodass ich dort ein Mock injizieren konnte was dann wiederum nicht das echte Popup triggern würde.&lt;/p&gt;&#10;&lt;p&gt;Ausschnitt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MsgBox&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;Re-implementation due to testing purposes: With this trick, we are able to mock these windows&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; so we do not have to wait for users to manually close the dialog, unblocking the application again.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;showinfo&lt;/span&gt;(self, title, message):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; showinfo(title&lt;span style="color:#f92672"&gt;=&lt;/span&gt;title, message&lt;span style="color:#f92672"&gt;=&lt;/span&gt;message)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;(self, title, message):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; showerror(title&lt;span style="color:#f92672"&gt;=&lt;/span&gt;title, message&lt;span style="color:#f92672"&gt;=&lt;/span&gt;message)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Als das funktionierte, habe ich die Änderungen voller Hoffnung nach Github gepusht. Und erhielt folgenden, wunderschönen Fehler:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;_tkinter.TclError: no display name and no $DISPLAY environment variable&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Das war zum Glück leicht zu verstehen - Tkinter wusste nicht, wo es die Fenster hinzeichnen sollte. Erscheint logisch, da der Server headless betrieben wird.&lt;/p&gt;&#10;&lt;p&gt;Also durchsuchte ich Foren und wurde mit nur einer Zeile Code belohnt, die dieses Problem sofort behob:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;uses: GabrielBB/xvfb-action@v1 # Diverts tkinter GUI to a virtual frame buffer (VFB)&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Seitdem führt Github Actions bei Pull request auf &lt;code&gt;main&lt;/code&gt; alle tests aus und endlich ergießt sich ein langersehnter Regen grüner Häkchen kühlend auf meine heißgelaufenen Hirnwindungen. Was für ein schönes Gefühl.&lt;/p&gt;&#10;</description></item><item><title>QR-codengrave V1.1 / V1.2</title><link>https://blog.schallbert.de/qr-codengrave1-1/</link><pubDate>Sun, 12 Feb 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/qr-codengrave1-1/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-02-12_qr-codengrave-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Splash screen of QR-codengrave V1.1"&#10; title="QR-codengrave V1.1 / V1.2" /&gt;&#10;&lt;h2 id="kurze-fakten-des-updates"&gt;Kurze Fakten des Updates&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schnellerer Gravuralgorithmus und -Pfadoptimierung&lt;/li&gt;&#10;&lt;li&gt;Diverse Bugfixes&lt;/li&gt;&#10;&lt;li&gt;Bedienbarkeit verbessert&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="demo-video"&gt;Demo Video&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/jFD7nxftVHjUy6LKPCFE3N"&#10; title="QR-codengrave Quick Demo"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;QR-codengrave Quick Demo&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/jFD7nxftVHjUy6LKPCFE3N" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="version-11"&gt;Version 1.1&lt;/h2&gt;&#10;&lt;h3 id="algoritmusoptimierung"&gt;Algoritmusoptimierung&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Die Zeilen des QR-Codes werden nun horizontal eingelesen&lt;/li&gt;&#10;&lt;li&gt;Die Ausgabe der Pfade verläuft nun &amp;ldquo;rechts nach links&amp;rdquo;, danach &amp;ldquo;links nach rechts&amp;rdquo; und reduziert damit Maschinenbewegungen&lt;/li&gt;&#10;&lt;li&gt;Linien werden Punkten vorgezogen, da sie schneller gefertigt werden können&lt;/li&gt;&#10;&lt;li&gt;Linien werden nun vertikal und horizontal berechnet, die längere Linie bekommt den Vorzug&lt;/li&gt;&#10;&lt;li&gt;Eilgänge werden nun per XY-Vektor gefahren und nicht mehr nur eindimensional, was Zeit spart&lt;/li&gt;&#10;&lt;li&gt;Der neue Pfadalgorithmus benötigt 20% weniger Maschinenzeit&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-02-12_qr-codengrave_algo.jpg" alt="Image: Algorithm comparison 1.0 vs. 1.1"&gt;&lt;/figure&gt;&#10;&lt;h3 id="bugfixes"&gt;Bugfixes&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Bei nicht vorliegendem Datensatz &lt;code&gt;Persistence.dat&lt;/code&gt; wird das Startbild nicht angezeigt (&lt;code&gt;qruwu.jpg&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;Crash bei Klick auf &lt;code&gt;Stop Draw&lt;/code&gt; wenn zuvor keine Zeichnung angefragt wurde&lt;/li&gt;&#10;&lt;li&gt;Crash bei Hinzufügen eines Werkzeuges bei leerer Werkzeugliste&lt;/li&gt;&#10;&lt;li&gt;Wird das Programm ohne Werkzeugauswahl gestartet, ist das dropdown-Menü gesperrt&lt;/li&gt;&#10;&lt;li&gt;Fehler in der QR-Code Größenschätzung&lt;/li&gt;&#10;&lt;li&gt;Fehler in der Blockbreitenberechnung bei Verwendung von V-Fräsern&lt;/li&gt;&#10;&lt;li&gt;Das gegenwärtig ausgewählte Werkzeug wird bei Start der Anwendung nicht im Dropdown-Feld angezeigt&lt;/li&gt;&#10;&lt;li&gt;QR-codes können größer dargestellt werden der Anzeigebereich&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="bedienbarkeit"&gt;Bedienbarkeit&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Default Werkzeugname geändert von &lt;code&gt;name&lt;/code&gt; nach &lt;code&gt;default&lt;/code&gt; zur besseren Verständlichkeit&lt;/li&gt;&#10;&lt;li&gt;Fräser mit geraden Schneiden (also kein V-cut) ist jetzt das Standardwerkzeug&lt;/li&gt;&#10;&lt;li&gt;Erstellen eines Offsets für den XY-Nullpunkt ist jetzt intuitiver gelöst (Y- nach unten, X+ nach Rechts)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="version-12"&gt;Version 1.2&lt;/h2&gt;&#10;&lt;p&gt;Kleine Änderungen zu 1.1, am Wichtigsten die Behebung eines Bugs, bei dem die Aufforderung zum Werkzeugwechsel verloren gehen kann. Außerdem werden die Winkel von V-Fräsern jetzt bei Berechnung von Pixelbreite und Größe der QR-Codes mit einbezogen.&lt;/p&gt;&#10;&lt;h3 id="bugfixes-1"&gt;Bugfixes&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Werkzeugauswahl &lt;code&gt;Tx&lt;/code&gt; und Werkzeugwechselkommando &lt;code&gt;M06&lt;/code&gt; befinden sich nun in derselben Zeile G-Code wie üblich und können so wie gewünscht Makros auslösen.&lt;/li&gt;&#10;&lt;li&gt;Das beim letzten Speichern verwendete Werkzeug wird nicht angezeigt, stattdessen das erste Werkzeug der Liste&lt;/li&gt;&#10;&lt;li&gt;Programmabsturz wenn versucht wird, den Jobstatus zu aktualisieren obwohl keine Gravurparameter vorliegen&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="bedienbarkeit-1"&gt;Bedienbarkeit&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Versionsnummer als Kommentar in G-code hinzugefügt&lt;/li&gt;&#10;&lt;li&gt;Prefix &lt;code&gt;qr_&lt;/code&gt; der Zieldatei im Datei-Speichern-Dialog hinzugefügt&lt;/li&gt;&#10;&lt;li&gt;Genauigkeit der Maschinenzeitabschätzung verbessert&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="download"&gt;Download&lt;/h2&gt;&#10;&lt;p&gt;Der aktuelle Release ist &lt;a href="https://github.com/Schallbert/QR-codengrave/releases" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hier&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zum Download verfügbar. Das Programm ist für die private Anwendung sowie Schulungszwecke kostenlos. Für weitere Informationen und Projektdokumentation stehen die &lt;a href="https://github.com/Schallbert/QR-codengrave" target="_blank" rel="noopener noreferrer" class="external-link"&gt;readme&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; sowie eine &lt;a href="https://blog.schallbert.de/projects/qr-codengrave/"&gt;Projektseite&lt;/a&gt; zur freien Verfügung.&lt;/p&gt;&#10;&lt;h2 id="upgrade-von-einer-früheren-version"&gt;Upgrade von einer früheren Version&lt;/h2&gt;&#10;&lt;p&gt;Es wird empfohlen, die Persistenzdatei von der alten Version in die neue Version zu übernehmen. Auf diese Weise werden gespeicherte Werkzeuglisten, Gravurparameter und XY-Offsets portiert. Und so geht&amp;rsquo;s:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Wechseln Sie in den Ordner, wo sich das QR-codengrave Programm befindet. Anschließend navigieren Sie zu &lt;code&gt;src/assets&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Kopieren Sie die Datei &lt;code&gt;Persistence.dat&lt;/code&gt; in einen temporären Ordner oder z.B. auf Ihren Desktop.&lt;/li&gt;&#10;&lt;li&gt;Entfernen Sie die alte Version durch Löschen des QR-codengrave Ordners.&lt;/li&gt;&#10;&lt;li&gt;Laden Sie die letzte Version herunter und verschieben Sie sie zur gewünschten Position auf Ihrem Rechner.&lt;/li&gt;&#10;&lt;li&gt;Verschieben Sie nun die vorab zwischengespeicherte &lt;code&gt;Persistence.dat&lt;/code&gt; in den &lt;code&gt;src/assets&lt;/code&gt; Ordner des aktualisierten Programms.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item><item><title>Videos mit dem VLC player konvertieren</title><link>https://blog.schallbert.de/video-conversion/</link><pubDate>Wed, 27 Oct 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/video-conversion/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-27_vlc-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: VLC media player icon"&#10; title="Videos mit dem VLC player konvertieren" /&gt;&#10;&lt;h2 id="website-medienerstellung"&gt;Website: Medienerstellung&lt;/h2&gt;&#10;&lt;p&gt;Für diese Website habe ich viele Mediendateien konvertiert. Die selbstgemachten Fotos sind sehr groß, was zu langen Ladezeiten der Seite und bildschirmfüllenden Bildern führt. Während es recht einfach ist, ihre Größe zu reduzieren und die JPEG-Komprimierung zu erhöhen, ist dies bei Videos schwieriger.&lt;/p&gt;&#10;&lt;h2 id="das-problem"&gt;Das Problem&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-27_wtf-vlc.jpg" alt="Image: VLCs Vorschau Effekte und Filter Fenster"&gt;&lt;/figure&gt;&#10;Hä, VLC?!&#10;Ich verwende den beliebten &lt;a href="https://www.videolan.org/vlc/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;vlc player&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und irgendwie habe ich einige Grundkonzepte nicht richtig verstanden. Entweder wird mein Video nicht wie geplant in Graustufen angezeigt, die Ausgabedatei ist leer (Verwendung von &lt;code&gt;H.264&lt;/code&gt; statt &lt;code&gt;H.265&lt;/code&gt; behebt das Problem), das Seitenverhältnis scheint verzerrt zu sein, oder das Video hat sich plötzlich gedreht, was ich weder wollte noch erwartet habe.&lt;/p&gt;&#10;&lt;h2 id="in-diesem-beitrag"&gt;In diesem Beitrag&lt;/h2&gt;&#10;&lt;p&gt;Eine Reihe von Schritten hilft mir, wie man einfache Aufgaben zur Konvertierung und rudimentäre Bearbeitung von Videos mit vlc umsetzt.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/video-conversion/#colors"&gt;Farben verändern oder auf Graustufen wechseln&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/video-conversion/#rotation"&gt;Das Video rotieren&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/video-conversion/#cropping"&gt;Ausschnitt eines Videos verwenden, Auflösung oder Seitenverhältnis anpassen&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/video-conversion/#cutting"&gt;Ein Video schneiden&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/video-conversion/#scaling"&gt;Größe des Videos auf dem Datenträger verringern&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="wie-funktioniert-das-mit-den-filtern"&gt;Wie funktioniert das mit den Filtern?&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-27_video-effects.jpg" alt="Image: VLC&amp;#39;s preview Effects and filters window"&gt;&lt;/figure&gt;&#10;&lt;h3 id="vorschau-der-filter"&gt;Vorschau der Filter&lt;/h3&gt;&#10;&lt;p&gt;VLC verfügt über eine Schnellvorschau für die gängigsten Filter im Menü &amp;ldquo;Werkzeuge -&amp;gt; Effekte und Filter&amp;rdquo;. Dies wirkt sich direkt auf das Video aus, das Sie ansehen, aber nicht auf Ihre gespeicherte Datei. Sie dient lediglich dazu, eine Vorschau zu erstellen und die Auswirkungen dieser Filter auf Ihr Video zu kontrollieren. In &lt;a href="https://www.videoproc.com/troubleshoot-vlc/how-to-crop-a-video-in-vlc.htm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;diesem VideoProc-Blogeintrag&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; finden Sie ausführliche Anleitungen - und, was noch wichtiger ist, viele Bilder, die Sie dabei unterstützen. Für die ungeduldige Fraktion: weiterlesen 😉&lt;/p&gt;&#10;&lt;h3 id="gefiltertes-video-in-datei-speichern"&gt;Gefiltertes Video in Datei speichern&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-27_profile.jpg" alt="Image: VLC&amp;#39;s conversion profile window for saving"&gt;&lt;/figure&gt;&#10;Um einen Filter zu speichern, den Sie für Ihre Vorschau ausgewählt haben, müssen Sie ihn zum Konvertierungsprofil hinzufügen. Solange sie angekreuzt bleiben, wird &lt;em&gt;jedes&lt;/em&gt; Speichern mit diesem Profil Ihr Video verändern, auch wenn Sie alle Änderungen, die Sie in der Vergangenheit an den Vorschaufiltern vorgenommen haben, rückgängig gemacht haben!&lt;/p&gt;&#10;&lt;h2 id="colors"&gt;Wie kann ich die Farben in meinem Video ändern?&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Öffnen Sie die Videodatei, die Sie ändern möchten.&lt;/li&gt;&#10;&lt;li&gt;Klicken Sie auf &amp;ldquo;Werkzeuge -&amp;gt; Effekte und Filter&amp;rdquo; (oder drücken Sie &amp;ldquo;Strg+E&amp;rdquo;)&lt;/li&gt;&#10;&lt;li&gt;Es öffnet sich ein Dialogfeld mit drei Registerkarten. Wählen Sie die Registerkarte &amp;ldquo;Videoeffekte&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Markieren Sie unter &amp;ldquo;Essentials&amp;rdquo; die Option &amp;ldquo;Bildanpassung&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Nehmen Sie Ihre Änderungen vor und klicken Sie auf &amp;ldquo;Speichern&amp;rdquo; und dann auf &amp;ldquo;Schließen&amp;rdquo;. Eine Vorschau des Effekts wird im Hauptfenster angezeigt.&lt;/li&gt;&#10;&lt;li&gt;Öffnen Sie die Wiedergabelistenansicht durch Drücken von &amp;ldquo;Wiedergabeliste umschalten&amp;rdquo; (Strg+L).&lt;/li&gt;&#10;&lt;li&gt;Klicken Sie mit der rechten Maustaste auf Ihr Video und wählen Sie &amp;ldquo;Speichern&amp;hellip;&amp;rdquo; im Kontextmenü.&lt;/li&gt;&#10;&lt;li&gt;Drücken Sie das Schraubenschlüssel-Symbol &amp;ldquo;Ausgewähltes Profil bearbeiten&amp;rdquo; und wählen Sie auf der Registerkarte &amp;ldquo;Videocodec&amp;rdquo; die Unterregisterkarte &amp;ldquo;Filter&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Markieren Sie den Filter &amp;ldquo;Bildeigenschaften&amp;rdquo;. Dadurch wird Ihre Änderung durchgeführt, sobald Sie Ihre Datei speichern.&lt;/li&gt;&#10;&lt;li&gt;Geben Sie eine Zieldatei ein und klicken Sie auf &amp;ldquo;Start&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Vergessen Sie nicht, den Filter &amp;ldquo;Bildeigenschaften&amp;rdquo; in Ihrem Profil zu deaktivieren, wenn Sie damit fertig sind. Nehmen Sie auch die Änderungen zurück, die Sie auf der Registerkarte &amp;ldquo;Videoeffekte&amp;rdquo; vorgenommen haben (Schritt 4).&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="rotation"&gt;Wie drehe ich mein Video?&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Öffnen Sie die Videodatei, die Sie ändern möchten.&lt;/li&gt;&#10;&lt;li&gt;Klicken Sie auf &amp;ldquo;Werkzeuge -&amp;gt; Effekte und Filter&amp;rdquo; (oder drücken Sie &amp;ldquo;Strg+E&amp;rdquo;)&lt;/li&gt;&#10;&lt;li&gt;Es öffnet sich ein Dialogfeld mit drei Registerkarten. Wählen Sie die Registerkarte &amp;ldquo;Videoeffekte&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Wählen Sie unter &amp;ldquo;Geometrie&amp;rdquo; die Option &amp;ldquo;Transformieren&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Nehmen Sie Ihre Änderungen vor und klicken Sie auf &amp;ldquo;Speichern&amp;rdquo; und dann auf &amp;ldquo;Schließen&amp;rdquo;. Eine Vorschau des Effekts wird im Hauptfenster angezeigt.&lt;/li&gt;&#10;&lt;li&gt;Öffnen Sie die Wiedergabelistenansicht durch Drücken von &amp;ldquo;Wiedergabeliste umschalten&amp;rdquo; (Strg+L).&lt;/li&gt;&#10;&lt;li&gt;Klicken Sie mit der rechten Maustaste auf Ihr Video und wählen Sie &amp;ldquo;Speichern&amp;hellip;&amp;rdquo; im Kontextmenü.&lt;/li&gt;&#10;&lt;li&gt;Drücken Sie das Schraubenschlüssel-Symbol &amp;ldquo;Ausgewähltes Profil bearbeiten&amp;rdquo; und wählen Sie auf der Registerkarte &amp;ldquo;Videocodec&amp;rdquo; die Unterregisterkarte &amp;ldquo;Filter&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Markieren Sie den Filter &amp;ldquo;Videotransformation&amp;rdquo;. Dadurch wird Ihre Änderung durchgeführt, sobald Sie Ihre Datei speichern.&lt;/li&gt;&#10;&lt;li&gt;Geben Sie eine Zieldatei ein und klicken Sie auf &amp;ldquo;Start&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Vergessen Sie nicht, das Häkchen beim Filter &amp;ldquo;Bildeigenschaften&amp;rdquo; in Ihrem Profil zu entfernen. Nehmen Sie auch die Änderungen zurück, die Sie auf der Registerkarte &amp;ldquo;Videoeffekte&amp;rdquo; vorgenommen haben.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="cropping"&gt;Wie kann ich mein Video zuschneiden?&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Öffnen Sie die Videodatei, die Sie ändern möchten.&lt;/li&gt;&#10;&lt;li&gt;Klicken Sie auf &amp;ldquo;Werkzeuge -&amp;gt; Effekte und Filter&amp;rdquo; (oder drücken Sie &amp;ldquo;Strg+E&amp;rdquo;)&lt;/li&gt;&#10;&lt;li&gt;Es öffnet sich ein Dialogfeld mit drei Registerkarten. Wählen Sie die Registerkarte &amp;ldquo;Videoeffekte&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Spielen Sie unter &amp;ldquo;Zuschneiden&amp;rdquo; mit den Pixelwerten herum und beobachten Sie das Vorschaufenster genau, bis Sie mit dem Zuschnitt zufrieden sind. Notieren oder merken Sie sich die Einstellungen, die Sie anwenden möchten. Schließen Sie den Dialog.&lt;/li&gt;&#10;&lt;li&gt;Klicken Sie auf &amp;ldquo;Werkzeuge -&amp;gt; Voreinstellungen&amp;rdquo; (oder drücken Sie &amp;ldquo;Strg+P&amp;rdquo;)&lt;/li&gt;&#10;&lt;li&gt;Wählen Sie auf der Registerkarte &lt;code&gt;Interface&lt;/code&gt; (Start) die Optionsschaltfläche &lt;code&gt;Show Settings - All&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Scrollen Sie nach unten zum Element &amp;ldquo;Video&amp;rdquo; und öffnen Sie &amp;ldquo;Filter&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Wählen Sie den &amp;ldquo;Video-Zuschneidefilter&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Suchen Sie das Element &lt;code&gt;Cropadd&lt;/code&gt; in der Filterliste auf der linken Seite. Geben Sie die Werte ein, die Sie sich in der Vorschau gemerkt/aufgeschrieben haben. Klicken Sie auf &amp;ldquo;Speichern&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Öffnen Sie die Wiedergabelistenansicht, indem Sie auf &amp;ldquo;Wiedergabeliste umschalten&amp;rdquo; (Strg+L) drücken. Klicken Sie dann mit der rechten Maustaste auf Ihr Video und wählen Sie &amp;ldquo;Speichern&amp;hellip;&amp;rdquo; aus dem Kontextmenü.&lt;/li&gt;&#10;&lt;li&gt;Drücken Sie das Schraubenschlüssel-Symbol &amp;ldquo;Ausgewähltes Profil bearbeiten&amp;rdquo; und wählen Sie auf der Registerkarte &amp;ldquo;Videocodec&amp;rdquo; die Unterregisterkarte &amp;ldquo;Filter&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Aktivieren Sie den Filter &amp;ldquo;Videoschnitt&amp;rdquo;. Dadurch wird die Änderung durchgeführt, sobald Sie Ihre Datei speichern.&lt;/li&gt;&#10;&lt;li&gt;Geben Sie eine Zieldatei ein und klicken Sie auf &amp;ldquo;Start&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Vergessen Sie nicht, das Häkchen bei &amp;ldquo;Video-Zuschnittfilter&amp;rdquo; in Ihrem Profil zu entfernen. Nehmen Sie auch die Änderungen zurück, die Sie in den Einstellungen unter &amp;ldquo;Filter&amp;rdquo; vorgenommen haben.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="cutting"&gt;Wie kürze ich mein Video?&lt;/h2&gt;&#10;&lt;p&gt;Hinweis: Eine ausführlichere Version dieses Themas findet sich in &lt;a href="https://www.movavi.io/how-to-trim-video-in-vlc-en/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;diesem Beitrag von movavi&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Öffnen Sie die Videodatei, die Sie ändern möchten.&lt;/li&gt;&#10;&lt;li&gt;Zeigen Sie die Schaltfläche &amp;ldquo;Aufnehmen&amp;rdquo; im VLC Player an (markieren Sie &amp;ldquo;Ansicht-&amp;gt;Erweiterte Steuerelemente&amp;rdquo;).&lt;/li&gt;&#10;&lt;li&gt;Springen Sie an die Stelle, an der das geschnittene Video beginnen soll.&lt;/li&gt;&#10;&lt;li&gt;Drücken Sie die rote Schaltfläche &amp;ldquo;Aufnehmen&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Spielen Sie die Videosequenz ab. Halten Sie an der Stelle an, an der der Zuschnitt enden soll.&lt;/li&gt;&#10;&lt;li&gt;VLCs Standardspeicherort für die Videoausgabe: &lt;code&gt;C:\Benutzer\Benutzername\Video&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="scaling"&gt;Wie ändere ich die Videoauflösung?&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Öffnen Sie die Videodatei, die Sie ändern möchten.&lt;/li&gt;&#10;&lt;li&gt;Öffnen Sie die Wiedergabelistenansicht, indem Sie &amp;ldquo;Wiedergabeliste umschalten&amp;rdquo; (Strg+L) drücken. Klicken Sie dann mit der rechten Maustaste auf Ihr Video und wählen Sie &amp;ldquo;Speichern&amp;hellip;&amp;rdquo; aus dem Kontextmenü.&lt;/li&gt;&#10;&lt;li&gt;Drücken Sie das Schraubenschlüssel-Symbol &amp;ldquo;Ausgewähltes Profil bearbeiten&amp;rdquo; und wählen Sie auf der Registerkarte &amp;ldquo;Videocodec&amp;rdquo; die Unterregisterkarte &amp;ldquo;Auflösung&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Geben Sie die Videogröße ein, in die Sie konvertieren möchten, und klicken Sie auf &amp;ldquo;Speichern&amp;rdquo;.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item><item><title>Favicon und Sammlungsgenerator</title><link>https://blog.schallbert.de/jekyll-icon-paginator/</link><pubDate>Sun, 13 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/jekyll-icon-paginator/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/favicon-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: favicon implementation on a website"&#10; title="Favicon und Sammlungsgenerator" /&gt;&#10;&lt;h3 id="hinzufügen-eines-tab-symbols---favicon"&gt;Hinzufügen eines &amp;ldquo;Tab&amp;rdquo;-Symbols - Favicon&lt;/h3&gt;&#10;&lt;p&gt;Ich war besorgt über eine merkwürdige Fehlermeldung, die Jekyll bei jedem Refresh meiner Seite ausgab: &lt;code&gt;[2021-06-13 22:07:43] ERROR '/favicon.ico' not found.&lt;/code&gt; Einige Online-Recherchen ergaben, dass dies weder ein Jekyll- noch ein Theme-Fehler ist, sondern ein Dateifehler. Siehe &lt;a href="https://blog.schallbert.de/projects/thissite/#favicon"&gt;hier&lt;/a&gt; wie ich das Icon für diese Seite implementiert habe.&lt;/p&gt;&#10;&lt;h3 id="erzeugung-von-kollektionen"&gt;Erzeugung von Kollektionen&lt;/h3&gt;&#10;&lt;p&gt;Ich habe herausgefunden, dass irgendwie die Sammlungserzeugung auf meiner Seite nicht funktioniert. Trotzdem geht das Layouting-Skript aber davon aus, dass es die Seiten vorbereitet hat. Wenn ich nun versuche, die Seite durch Drücken der Schaltfläche &amp;ldquo;Zurück&amp;rdquo; aufzurufen, erhalte ich einen 404-Fehler. Bis jetzt war ich nicht in der Lage, dies zu beheben, weil die Erzeugung stillschweigend fehlschlägt und ich nicht weiß, wo.&#10;Ich schalte sie vorerst komplett ab.&lt;/p&gt;&#10;</description></item><item><title>Markdown zu PDF konvertieren</title><link>https://blog.schallbert.de/md-export-pdf/</link><pubDate>Wed, 09 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/md-export-pdf/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/markdown-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: markdown to pdv converter extension for VS code"&#10; title="Markdown zu PDF konvertieren" /&gt;&#10;&lt;h3 id="die-software"&gt;Die Software&lt;/h3&gt;&#10;&lt;p&gt;Ich habe die Visual Studio Code-Erweiterung &lt;a href="https://github.com/yzane/vscode-markdown-pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Yzane&amp;rsquo;s Markdown PDF&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gewählt. Einfach zu installieren, einfach zu benutzen, gute Dokumentation. Nur die Bilder&amp;hellip; naja&amp;hellip; wurden nicht angezeigt.&lt;/p&gt;&#10;&lt;h3 id="auch-die-bilder-werden-konvertiert"&gt;Auch die Bilder werden konvertiert&lt;/h3&gt;&#10;&lt;p&gt;Die relativen Links zu den Bildern unterscheiden sich zwischen Jekylls Interpretation und der Vorstellung des PDF-Generators. Ich musste die Markdown-Zieldatei in das Stammverzeichnis der Website verschieben und ein &lt;code&gt;.&lt;/code&gt; vor meinem &lt;code&gt;/assets&lt;/code&gt; hinzufügen, damit es funktioniert. Die Seitenumbrüche sind &amp;hellip; meh&amp;hellip; aber ich bin im Moment nicht bereit, mehr Arbeit hineinzustecken, also ist das gut genug für meine Zwecke.&lt;/p&gt;&#10;&lt;h3 id="ein-problem-mit-posts-die-nicht-angezeigt-werden"&gt;Ein Problem mit Posts, die nicht angezeigt werden?&lt;/h3&gt;&#10;&lt;p&gt;Gerade als ich diesen sechsten Beitrag von mir veröffentlicht habe, sehe ich, dass irgendwie nur Seite 2 der Beitragsübersicht angezeigt wird (sieht aus wie 5 Beiträge pro Seite) und ich kann nicht zu Seite 1 mit den &amp;ldquo;neuesten&amp;rdquo; Beiträgen wechseln: &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-06-09_stuckonpage2.jpg" alt="terminal output"&gt;&lt;/figure&gt;. Hm.&lt;/p&gt;&#10;</description></item><item><title>Jekyll: Bilder</title><link>https://blog.schallbert.de/jekyll-images/</link><pubDate>Mon, 07 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/jekyll-images/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/git_mv-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: github move operation for renaming"&#10; title="Jekyll: Bilder" /&gt;&#10;&lt;h3 id="der-trick"&gt;Der Trick&lt;/h3&gt;&#10;&lt;p&gt;Als ich einige Bilder zu meinen Inhalten hinzufügte, war alles in Ordnung, bis ich sie auf GitHub veröffentlichte. Die meisten Bilder waren verschwunden und ich konnte nicht herausfinden, warum. Eine &lt;a href="https://stackoverflow.com/questions/41468951/images-not-displaying-in-github-pages#41469181" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Suche&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ergab, dass Github zwischen Groß- und Kleinschreibung unterscheidet und einige meiner Bildpfade nicht alle kleingeschrieben waren. Dahinter vermute ich Unterschiede zwischen Windows/Unix, da sich die beiden Betriebssysteme hier unterschiedlich verhalten.&lt;/p&gt;&#10;&lt;h3 id="ändern-von-ordnernamen-in-git"&gt;Ändern von Ordnernamen in Git&lt;/h3&gt;&#10;&lt;p&gt;Es funktionierte danach immer noch nicht, weil Git meine Umbenennung nicht erkannte. Ich musste zuerst den umbenannten Ordner in einem anderen Unterverzeichnis erstellen, &lt;code&gt;git mv folder/tArGet* target&lt;/code&gt; verwenden, um diesen an einen temporären Ort zu bringen, &lt;code&gt;tArGet&lt;/code&gt; löschen und &lt;code&gt;target&lt;/code&gt; an die Stelle verschieben, an der &lt;code&gt;tArGet&lt;/code&gt; vorher war. Nur dies hat das Problem behoben und jetzt sind github und meine lokalen Dateien wieder synchron.&lt;/p&gt;&#10;</description></item><item><title>Jekyll Inhaltsverzeichnis</title><link>https://blog.schallbert.de/jekyll-toc/</link><pubDate>Sun, 06 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/jekyll-toc/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/desktop-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Schallbert&amp;#39;s Desktop"&#10; title="Jekyll Inhaltsverzeichnis" /&gt;&#10;&lt;h2 id="inhaltsverzeichnis"&gt;Inhaltsverzeichnis&lt;/h2&gt;&#10;&lt;p&gt;Es gibt eine einfache Lösung, um das Inhaltsverzeichnis der Seite zu fixieren:&#10;&lt;code&gt;toc_sticky: true&lt;/code&gt; zu den Standardeinstellungen oder dem Inhaltsverzeichnis der jeweiligen Seite hinzufügen. Wie ich das Schlüsselwort gefunden habe? Durch die Suche nach &amp;ldquo;toc&amp;rdquo; im Stammverzeichnis des Themes und ein wenig Scrollen durch die Layout-Skizzen. Ich änderte die ursprüngliche Seitennavigation in der linken Seitenleiste und tauschte sie gegen eine Dummy-Implementierung einer Galerie / Audio / Video-Inhaltsanzeige aus.&lt;/p&gt;&#10;&lt;h3 id="toc-auf-der-linken-seite"&gt;ToC auf der linken Seite&lt;/h3&gt;&#10;&lt;p&gt;Um zu verstehen, wie man das Inhaltsverzeichnis anders als in diesem Theme vorgegeben auf der linken Seite erstellt, lesen Sie bitte &lt;a href="https://blog.schallbert.de/projects/thissite/#sidebar"&gt;dieses Kapitel&lt;/a&gt; des mySite-Projekts weiter.&lt;/p&gt;&#10;&lt;h2 id="profil-des-autors"&gt;Profil des Autors&lt;/h2&gt;&#10;&lt;p&gt;Ich habe beschlossen, dass mein &lt;code&gt;author_profile&lt;/code&gt; nur auf Seiten wie &lt;a href="https://blog.schallbert.de/about/"&gt;about&lt;/a&gt;, &lt;a href="https://blog.schallbert.de/legal/"&gt;legal info&lt;/a&gt; und nicht auf den typischen Inhaltsseiten angezeigt werden soll. Also schrieb ich &lt;code&gt;author_profile: false&lt;/code&gt; für die Seiten, auf denen es nicht angezeigt werden sollte - ohne Erfolg. Warum?&#10;Ich hatte es in der &lt;code&gt;config.yml&lt;/code&gt;, Abschnitt &lt;code&gt;type defaults&lt;/code&gt; gesetzt, aber es sollte eigentlich in der Frontmatter der einzelnen Dateien stehen.&lt;/p&gt;&#10;</description></item><item><title>Jekyll `-incremental` Option</title><link>https://blog.schallbert.de/jekyll-items-update/</link><pubDate>Thu, 03 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/jekyll-items-update/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/jekyll-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Jekyll logo"&#10; title="Jekyll `-incremental` Option" /&gt;&#10;&lt;h3 id="die-kategorie-neueste-beiträge"&gt;die Kategorie &amp;ldquo;Neueste Beiträge&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Ich habe den Grund herausgefunden, warum mein letzter Artikel auf der Landing Page auf meinem lokalen Rechner nicht angezeigt wurde, aber auf dem Server problemlos funktionierte. Es liegt daran, dass ich lokal den Befehl &lt;code&gt;$ bundle exec jekyll serve --incremental&lt;/code&gt; mit der aktiven &lt;code&gt;--incremental&lt;/code&gt; build Option verwende, um die Rebuilds für schnellere Testläufe zu beschleunigen. Aber das Problem liegt hier:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-06-03_jekyll_incremental.jpg" alt="image: terminal output"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Bei einer inkrementellen Ausgabe scheint Jekyll den Ordner &lt;code&gt;_posts&lt;/code&gt; nicht nach neuen Einträgen zu durchsuchen, da die Seite, auf der sie erscheinen, nicht geändert sondern neu angelegt wurde.&#10;Mein Fazit ist, dass ich die Option &lt;code&gt;--incremental&lt;/code&gt; in Zukunft mit Vorsicht und nur für Änderungen bereits bestehender Artikel verwenden werde.&lt;/p&gt;&#10;&lt;h3 id="die-bilder"&gt;Die Bilder&lt;/h3&gt;&#10;&lt;p&gt;Ich habe an meinen Bildern unter &lt;code&gt;assets/images&lt;/code&gt; gearbeitet. Sie benötigen recht viel Speicherplatz, so dass die Ladezeiten der Seite zu lang werden. Ich benutze &lt;a href="https://www.getpaint.net/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;paint.net&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, um sie auf eine Größe zu schrumpfen, die ich für vertretbar halte, und speichere sie als &lt;code&gt;.jpg&lt;/code&gt; mit maximaler Kompression und einigen Kompromissen bei der Bildqualität. Auf diese Weise benötigen die meisten meiner Bilder weniger als 10% der ursprünglichen Größe in &lt;code&gt;kB&lt;/code&gt;.&lt;/p&gt;&#10;</description></item><item><title>MinimalMistakes theme - Seitendesign</title><link>https://blog.schallbert.de/jekyll-open-items/</link><pubDate>Wed, 02 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/jekyll-open-items/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/mmistakes-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: MinimalMistakes logo"&#10; title="MinimalMistakes theme - Seitendesign" /&gt;&#10;&lt;h2 id="die-landing-page"&gt;Die &amp;ldquo;Landing Page&amp;rdquo;&lt;/h2&gt;&#10;&lt;p&gt;Endlich. Indem ich den Layout-Typ auf &lt;code&gt;single&lt;/code&gt; änderte, konnte ich ein Header-Bild als Typ &lt;code&gt;overlay&lt;/code&gt; einfügen!&#10;Die Schaltflächen werden jetzt innerhalb des Bildes angezeigt. Das Bild ändert sich automatisch mit der Displaygröße und -ausrichtung aufgrund der im Ordner &lt;code&gt;_sass&lt;/code&gt; definierten visuellen Designs.&lt;/p&gt;&#10;&lt;h3 id="bild-der-hauptseite"&gt;Bild der Hauptseite&lt;/h3&gt;&#10;&lt;p&gt;Ich war dabei zu verzweifeln: meine &amp;ldquo;Landing Page&amp;rdquo; zeigte einfach kein Bild mehr an, egal wie sehr ich versuchte, die &lt;code&gt;home.md&lt;/code&gt; Datei zu ändern. Der Grund dafür war, dass es eine Standard-&amp;ldquo;index.html&amp;rdquo;-Datei gab, die mit der Vorlage geliefert wurde und die anscheinend Vorrang hatte. Nun habe ich meine Änderungen einfach direkt dort eingefügt - funktioniert einwandfrei.&#10;Übrigens, die Höhe des Overlay-Bildes wird automatisch in Abhängigkeit von Inhalt und Höhe des Overlay-Textes gewählt.&lt;/p&gt;&#10;&lt;h3 id="hinzufügen-einer-seitennavigation"&gt;Hinzufügen einer Seitennavigation&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Einfache Lösung:&lt;code&gt;toc: true&lt;/code&gt; in die YAML-Struktur des Markdown-Dokument für den gewünschten Artikel einfügen. Leider wird das Inhaltsverzeichnis statisch auf der Webseite angezeigt, sodass es beim Herunterscrollen verschwindet.&lt;/li&gt;&#10;&lt;li&gt;Schwierige Lösung: Fügen Sie die Seitennavigation in die linke Seitenleiste ein. Wie das geht? &lt;a href="https://blog.schallbert.de/projects/thissite/#navigation"&gt;Siehe hier&lt;/a&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="neueste-beiträge"&gt;Neueste Beiträge&lt;/h3&gt;&#10;&lt;p&gt;Im Moment erscheinen meine Artikel nicht in der Spalte &amp;ldquo;Neueste Beiträge&amp;rdquo; auf der Hauptseite. Das liegt daran, dass ich den Dienst neu starten müsste, weil die Beiträge statisch generiert werden &lt;a href="https://blog.schallbert.de/jekyll-items-update/"&gt;Richtig, Jekyll ist ein &lt;strong&gt;statischer&lt;/strong&gt; Seitengenerator.&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>ClickEncoder</title><link>https://blog.schallbert.de/projects/encoder/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/encoder/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/encoder/encoder-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Encoder on Breadboard"&#10; title="ClickEncoder" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeitsgrad: Mittel 3/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: 0€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~6h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="überblick"&gt;Überblick&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://raw.githubusercontent.com/isocpp/logos/64ef037049f87ac74875dbe72695e59118b52186/cpp_logo.svg" alt="Image: C&amp;#43;&amp;#43; logo"&gt;&lt;/figure&gt;&#10;Bibliothek für das &lt;a href="https://www.arduino.cc/en/Reference/Libraries" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Arduino Framework&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, geschrieben in C++.&lt;/p&gt;&#10;&lt;p&gt;Der Quellcode selbst besteht nur aus ein paar hundert Zeilen. Er ist leichtgewichtig und leistungsfähig, da der verbesserte Algorithmus weniger Berechnungen benötigt und gleichzeitig eine bessere Zustandsinterpretation bietet als das ursprüngliche Projekt, von dem sie abgezweigt ist. Die Bibliothek ist offiziell auf &lt;a href="https://platformio.org/lib/show/11808/ClickEncoder" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PlatformIO IDE&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; veröffentlicht und wird auf &lt;a href="https://github.com/Schallbert/encoder" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gehostet und gepflegt. Sie enthält Beispiele sowohl für Arduino- als auch für PlatformIO-IDEs und bietet Unittests, die nach der Anpassung an die spezifische Anwendung ausgeführt werden können.&lt;/p&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Ich habe zunächst die von &lt;a href="https://github.com/0xPIT/encoder" target="_blank" rel="noopener noreferrer" class="external-link"&gt;0xPIT&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; veröffentlichte Originalbibliothek verwendet, aber diese verursachte bei mir einige Compiler-Warnungen, außerdem waren die Messwerte des Encoders nicht einwandfrei und manchmal gab es Jitter oder zurückspringende Werte, was ich nicht akzeptieren wollte.&lt;/p&gt;&#10;&lt;h2 id="lösung"&gt;Lösung&lt;/h2&gt;&#10;&lt;p&gt;Da das Archiv ungepflegt aussah und meine Anfrage für Verbesserungen nicht beantwortet wurde, beschloss ich, diese Probleme selbst zu beheben. Ich habe also behutsam einen Teil des Codes umgeschrieben. Für mein damaliges Projekt benötigte ich auch ein &amp;ldquo;Wiederholungssignal&amp;rdquo;, wenn eine Taste kontinuierlich gedrückt wird, also fügte ich auch dies hinzu. Schließlich trennte ich den Encoder-Algorithmus von der Tastenbehandlung und stellte separate Klassen bereit, so dass nun bei Bedarf jede beliebige Kombination aus Taste und Encoder verwendet werden kann, jeweils mit konfigurierbarem Verhalten.&lt;/p&gt;&#10;&lt;h2 id="encoder-algorithmus-erklärt"&gt;Encoder Algorithmus erklärt&lt;/h2&gt;&#10;&lt;h2 id="api"&gt;API&lt;/h2&gt;&#10;&lt;p&gt;Die API von ClickEncoder ist einfach und leicht zu verstehen: &lt;code&gt;service()&lt;/code&gt; führt die Geschäftslogik aus und die Zustandsinterpretation durch. Diese Methode muss regelmäßig ausgeführt werden, um zu überprüfen, ob der Encoder seit seinem letzten Aufruf bewegt wurde, in welche Richtung, wie viele Schritte, und diese Werte dann in einer Akkumulatorvariablen speichern.&#10;&lt;code&gt;getIncrement()&lt;/code&gt; gibt die Schrittänderungen des Encoders seit dem letzten Aufruf zurück. &lt;code&gt;getAccumulate()&lt;/code&gt; gibt die Summe der Inkremente seit dem Start zurück. &lt;code&gt;getButton()&lt;/code&gt; gibt den aktuellen Button-Status zurück.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;service&lt;/span&gt;();&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int16_t&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getIncrement&lt;/span&gt;();&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int16_t&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getAccumulate&lt;/span&gt;();&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Button&lt;span style="color:#f92672"&gt;::&lt;/span&gt;eButtonStates getButton();&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="encoder-status"&gt;Encoder-Status&lt;/h3&gt;&#10;&lt;p&gt;Der Encoder verwendet einen &lt;a href="https://de.wikipedia.org/wiki/Gray_code" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gray Code&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Kodierung der Schritte. &lt;code&gt;getBitCode()&lt;/code&gt; wandelt diesen Code, der über die Hardware-Pins A und B des Encoders übertragen wird, in eine &amp;ldquo;0&amp;hellip;3&amp;rdquo;-Rasten-Interpretation um, wobei die Zahlen bei einer Rechtsdrehung ansteigen und bei einer Linksdrehung abfallen. Dies geschieht effizient ohne Verwendung von &lt;code&gt;if()&lt;/code&gt; oder anderer Verzweigungslogik.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; Encoder&lt;span style="color:#f92672"&gt;::&lt;/span&gt;getBitCode()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// GrayCode convert&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// !A &amp;amp;&amp;amp; !B --&amp;gt; 0&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// !A &amp;amp;&amp;amp; B --&amp;gt; 1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// A &amp;amp;&amp;amp; B --&amp;gt; 2&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// A &amp;amp;&amp;amp; !B --&amp;gt; 3&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; currentEncoderRead &lt;span style="color:#f92672"&gt;=&lt;/span&gt; digitalRead(pinA);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentEncoderRead &lt;span style="color:#f92672"&gt;|=&lt;/span&gt; (currentEncoderRead &lt;span style="color:#f92672"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// bit0 of result to be inverted if set&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentEncoderRead &lt;span style="color:#f92672"&gt;^=&lt;/span&gt; digitalRead(pinB);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; currentEncoderRead;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich habe hier mit einem Trick gearbeitet, sodass man sich den Code-Schnipsel oben möglicherweise mehrfach ansehen muss. Der Zustand von &lt;code&gt;PinA&lt;/code&gt; wird auf Bit1 von &lt;code&gt;currentEncoderRead&lt;/code&gt; geschrieben, indem ich es mit der &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;-Operation nach links schiebe. &lt;em&gt;Gleichzeitig bleibt der Status des 0. Bits durch &lt;code&gt;|=&lt;/code&gt; aber erhalten&lt;/em&gt;. Würde ich jetzt &lt;code&gt;PinB&lt;/code&gt; einfach logisch &lt;code&gt;&amp;amp;=&lt;/code&gt; anbinden, dann würde ich den Gray-Code verletzen, nachdem &lt;code&gt;A&amp;amp;&amp;amp;B=2&lt;/code&gt; sein sollte und nicht &lt;code&gt;3&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Sind &lt;code&gt;A&lt;/code&gt; und &lt;code&gt;B&lt;/code&gt; also beide nicht gesetzt, bleibt das Ergebnis &lt;code&gt;0&lt;/code&gt;, denn &lt;code&gt;00^00=00=0&lt;/code&gt;. Ist nur &lt;code&gt;A&lt;/code&gt; gesetzt, &lt;code&gt;B&lt;/code&gt; aber nicht, so lautet das Ergebnis &lt;code&gt;11^00=11=3&lt;/code&gt;. Wenn &lt;code&gt;B&lt;/code&gt; gesetzt ist, &lt;code&gt;A&lt;/code&gt; aber nicht, ergibt sich &lt;code&gt;00^01=01=1&lt;/code&gt;. Sind beide gesetzt, schließlich &lt;code&gt;11^01=10=2&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Für weitere Informationen zum Thema logische Binäroperationen siehe &lt;a href="https://blog.schallbert.de/projects/fireplace/#galois-lfsr-implementierung"&gt;mein Steckbrett-Lagerfeuer Projekt&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="zustandsinterpretation"&gt;Zustandsinterpretation&lt;/h3&gt;&#10;&lt;p&gt;Nachdem der Bitcode gelesen wurde, wird die Variable &lt;code&gt;rawMovement&lt;/code&gt; gesetzt, die uns sagt, wie weit der Encoder zwischen zwei Durchläufen der &lt;code&gt;service()&lt;/code&gt; Routine gedreht wurde. Hier sieht man, dass dieser Schritt zeitkritisch ist: Es kommt zu Aliasing-Fehlern, wenn der Geber schneller einmal alle Positionen wechselt, als dass &lt;code&gt;service()&lt;/code&gt; erneut aufgerufen wird. In diesem Falle würden Schritte verloren gehen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; Encoder&lt;span style="color:#f92672"&gt;::&lt;/span&gt;handleEncoder()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; encoderRead &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getBitCode();&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// bit0 set = status changed, bit1 set = &amp;#34;overflow 3&amp;#34; where it goes 0-&amp;gt;3 or 3-&amp;gt;0&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; rawMovement &lt;span style="color:#f92672"&gt;=&lt;/span&gt; encoderRead &lt;span style="color:#f92672"&gt;-&lt;/span&gt; lastEncoderRead;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastEncoderRead &lt;span style="color:#f92672"&gt;=&lt;/span&gt; encoderRead;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// This is the uint-&amp;gt;int magic, converts raw to: -1 counterclockwise, 0 no turn, 1 clockwise&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;int8_t&lt;/span&gt; signedMovement &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ((rawMovement &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&lt;/span&gt; (rawMovement &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;));&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; encoderAccumulate &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; signedMovement;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; encoderAccumulate &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; handleAcceleration(signedMovement);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hier ein weiterer Trick: &lt;code&gt;signedMovement&lt;/code&gt; wird berechnet, indem die Bewegung in eine vorzeichenbehaftete Ganzzahl umgewandelt wird, die &lt;code&gt;0&lt;/code&gt; als &amp;ldquo;nicht gedreht&amp;rdquo;, &lt;code&gt;-1&lt;/code&gt; als &amp;ldquo;links gedreht&amp;rdquo; und &lt;code&gt;1&lt;/code&gt; als &amp;ldquo;rechts gedreht&amp;rdquo; darstellt. Dabei wird das Verhalten bei Integer-Überlauf ausgenutzt, in Kombination mit der Interpretation von &amp;ldquo;signedness&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;Abschließend wird die Akkumulation durchgeführt und die Beschleunigung, sofern konfiguriert, dem Akkumulator hinzugefügt, je nachdem, wie schnell der Geber gedreht wird.&lt;/p&gt;&#10;&lt;p&gt;Um diese beiden Methoden ohne Verzweigungen zu programmieren, habe ich mehrere Abende Grübelei und praktisch einen halben Block kariertes Papier benötigt. Offen gesagt haben mich diese paar Zeilen Code an den Rande der Leistungsfähigkeit meines Hirns gebracht. Ich habe das Programmieren eben nicht von der Pike auf gelernt 😅&lt;/p&gt;&#10;&lt;h3 id="die-methode-getaccumulate"&gt;Die Methode getAccumulate()&lt;/h3&gt;&#10;&lt;p&gt;Sie ist wirklich einfach. Sie gibt nur den internen akkumulierten Wert zurück, der der Konfiguration des Encoders entspricht, wie viele Ticks einen Schritt ausmachen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int16_t&lt;/span&gt; Encoder&lt;span style="color:#f92672"&gt;::&lt;/span&gt;getAccumulate()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; (encoderAccumulate &lt;span style="color:#f92672"&gt;/&lt;/span&gt; stepsPerNotch);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="die-methode-getincrement"&gt;Die Methode getIncrement()&lt;/h3&gt;&#10;&lt;p&gt;Gibt zurück um wie viele Ticks sich der Encoder seit dem letzten Aufruf dieser Methode bewegt hat.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int16_t&lt;/span&gt; Encoder&lt;span style="color:#f92672"&gt;::&lt;/span&gt;getIncrement()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;int16_t&lt;/span&gt; accu &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getAccumulate();&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;int16_t&lt;/span&gt; encoderIncrements &lt;span style="color:#f92672"&gt;=&lt;/span&gt; accu &lt;span style="color:#f92672"&gt;-&lt;/span&gt; lastEncoderAccumulate;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastEncoderAccumulate &lt;span style="color:#f92672"&gt;=&lt;/span&gt; accu;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; (encoderIncrements);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="ermittlung-des-tasterzustands"&gt;Ermittlung des Tasterzustands&lt;/h2&gt;&#10;&lt;h3 id="die-methode-handlebutton"&gt;Die Methode handleButton()&lt;/h3&gt;&#10;&lt;p&gt;Alle Tasterzustände leiten sich von zwei Basiszuständen ab: gedrückt oder nicht gedrückt. Dies spiegelt sich in der Logik wider:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; Button&lt;span style="color:#f92672"&gt;::&lt;/span&gt;handleButton()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (lastGetButtonCount &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; ENC_BUTTONINTERVAL)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastGetButtonCount &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (digitalRead(pinBTN) &lt;span style="color:#f92672"&gt;==&lt;/span&gt; pinActiveState)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handleButtonPressed();&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handleButtonReleased();&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (doubleClickTicks &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;--&lt;/span&gt;doubleClickTicks;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Der Taster kann so konfiguriert werden, dass er seltener ausgelesen wird als der Encoder. Wenn dies der Fall ist, bleibt diese Methode zwischenzeitlich ohne Aktion.&lt;/p&gt;&#10;&lt;h3 id="taste-gedrückt"&gt;Taste gedrückt:&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;handleButtonPressed()&lt;/code&gt; kann entweder &lt;code&gt;Closed&lt;/code&gt;, &lt;code&gt;Held&lt;/code&gt; oder &lt;code&gt;LongPressRepeat&lt;/code&gt; zurückgeben, je nachdem wie der Taster konfiguriert ist und wie lange er bereits gedrückt wurde.&lt;/p&gt;&#10;&lt;h3 id="taste-nicht-gedrückt"&gt;Taste nicht gedrückt:&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;handleButtonReleased()&lt;/code&gt; kann entweder &lt;code&gt;Clicked&lt;/code&gt;, &lt;code&gt;Released&lt;/code&gt; oder &lt;code&gt;Doubleclicked&lt;/code&gt; zurückgeben, abhängig von der Konfiguration des Tasters und der Anzahl der Klicks innerhalb einer bestimmten Zeit.&lt;/p&gt;&#10;&lt;h3 id="konfiguration-der-bibliothek"&gt;Konfiguration der Bibliothek&lt;/h3&gt;&#10;&lt;p&gt;Das Verhalten der Encoder/Tastenerkennung kann in der Datei &lt;code&gt;encoder.h&lt;/code&gt; mit Hilfe der unten aufgeführten Konstanten geändert werden. Die Werte wurden bereits optimiert und fühlen sich, zumindest für mich, natürlich an. Sie sollten geändert werden, wenn Sie ein anderes Serviceintervall als &lt;code&gt;1ms&lt;/code&gt; verwenden möchten, da diese Werte alle dieses Intervall als Referenz nehmen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// ----------------------------------------------------------------------------&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Acceleration configuration (for 1ms calls to ::service())&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;//&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;constexpr&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; ENC_ACCEL_START &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;150&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// The smaller this value, the quicker you must turn to activate acceleration.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;constexpr&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; ENC_ACCEL_SLOPE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;75&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// the smaller this value, the stronger the acceleration will manipulate values.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Button configuration (values for 1ms timer service calls)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;//&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;constexpr&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; ENC_BUTTONINTERVAL &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// check button every x ms, also debouce time&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;constexpr&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint16_t&lt;/span&gt; ENC_DOUBLECLICKTIME &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;400&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// second click within x ms&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;constexpr&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint16_t&lt;/span&gt; ENC_LONGPRESSREPEATINTERVAL &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// reports repeating-held every x ms&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;constexpr&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint16_t&lt;/span&gt; ENC_HOLDTIME &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1200&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// report held button after x ms&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// ----------------------------------------------------------------------------&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="verwenden-sie-sie-in-ihrem-projekt"&gt;Verwenden Sie sie in Ihrem Projekt!&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Die Bibliothek ist eine Open-Source-Software, die unter der MIT-Lizenz steht.&lt;/li&gt;&#10;&lt;li&gt;PlatformIO-Benutzer? Ganz einfach, suchen Sie sie in der Ansicht &amp;lsquo;Bibliotheken&amp;rsquo;, die Tags sind Arduino, Encoder, Schallbert. Dann ist es nur noch ein Klick, um sie zu Ihrer Lösung hinzuzufügen.&lt;/li&gt;&#10;&lt;li&gt;Alternativ können Sie auch das &lt;a href="https://github.com/Schallbert/encoder" target="_blank" rel="noopener noreferrer" class="external-link"&gt;repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; klonen oder forken und die Header/Cpp-Datei nach eigenem Ermessen verwenden.&lt;/li&gt;&#10;&lt;li&gt;Sie möchten etwas beitragen? Vielleicht haben Sie eine effizientere Zustandsberechnung für die Taster? Ich würde mich freuen, Ihren Pull Request in meinem Posteingang zu finden.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>Diese Website programmieren</title><link>https://blog.schallbert.de/projects/thissite/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/thissite/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/desktop-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Schallbert&amp;#39;s Desktop"&#10; title="Diese Website programmieren" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeitsgrad: Mittel 3/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: 0€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~15h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="die-idee"&gt;Die Idee&lt;/h2&gt;&#10;&lt;p&gt;Die Idee entstand durch mein &lt;a href="https://blog.schallbert.de/projects/mobfobamp/"&gt;MobFobAmp&lt;/a&gt;-Projekt, für das ich kein Github-Repository hatte, da es keinen Code oder ähnliche Dokumentation gab. Also beschloss ich eine Website einzurichten, auf der ich Anleitungen für dieses Open-Hardware-Projekt bereitstellen kann, damit andere das Projekt nachbauen können. Ich wollte eine einfach zu hostende Lösung, die nicht zu viel Arbeit beim Einrichten erfordert. Da ich keinen eigenen Server habe, habe ich um Hilfe gebeten. &lt;a href="https://github.com/moritzmar" target="_blank" rel="noopener noreferrer" class="external-link"&gt;moritzmar&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; empfahl mir, für den Anfang &amp;ldquo;Github-Pages&amp;rdquo; als Site-Host zu verwenden, um eine einfache automatische Bereitstellung und globale Reichweite zu gewährleisten. Also habe ich das mal ausprobiert.&lt;/p&gt;&#10;&lt;h2 id="erste-schritte"&gt;Erste Schritte&lt;/h2&gt;&#10;&lt;p&gt;Eine statische Site kam mir genau Recht. Ohne die Notwendigkeit einer Datenbank oder eines großen Content-Management-Frameworks bleibt mein Site-Layout einfach und minimalistisch; ich wollte am Anfang nur dieses eine Projekt hosten.&#10;&lt;a href="https://docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll/about-github-pages-and-jekyll" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github empfiehlt die Verwendung von Jekyll&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://upload.wikimedia.org/wikipedia/commons/4/42/Jekyll_%28software%29_Logo.png" alt="Image: Jekyll Logo"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Jekyll Logo&lt;/span&gt;&lt;a&#10; href="https://commons.wikimedia.org/wiki/File:Jekyll_%28software%29_Logo.png#file"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;als Website-Generator. Ich hatte einige Probleme mit dem Windows Subsystem für Linux (WSL), da es meine Terminaleingaben nicht akzeptierte, also folgte ich der &lt;a href="https://jekyllrb.com/docs/installation/windows/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und verwendete den RubyInstaller, der bei mir sofort funktionierte.&lt;/p&gt;&#10;&lt;h2 id="einrichtung-der-site"&gt;Einrichtung der Site&lt;/h2&gt;&#10;&lt;p&gt;Mir wurde klar, dass ich vielleicht mehrere meiner Projekte dokumentieren möchte. Deshalb kam ich auf die Idee, Projekte in Gruppen zusammenzufassen, jede mit einer Überschrift, einem kleinen Text und einem Miniaturbild. Wenn man darauf klickt, kommt man auf die jeweilige Projektseite. Das würde eine Menge Programmierarbeit für die Seiteneinrichtung und das Layout bedeuten, mit der ich nicht allzu viel Zeit verbringen wollte - meine anderen Projekte warten&amp;hellip;&lt;/p&gt;&#10;&lt;h2 id="erster-versuch-mit-themes"&gt;Erster Versuch mit Themes&lt;/h2&gt;&#10;&lt;p&gt;Also folgte ich einer weiteren Anregung von Github-Pages: Die Verwendung von vordefinierten &amp;ldquo;Themes&amp;rdquo;.&#10;Zuerst habe ich das &lt;a href="http://jekyllthemes.org/themes/agency/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Agency Theme&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; verwendet. Es hat eine tolle Landing Page mit einem großen Bild und unterstützt Sammlungen, die das von mir gewünschte Gruppieren von Projekten ermöglichen. Leider ist das Thema ein &amp;ldquo;Remote-Thema&amp;rdquo;, so dass die Anpassungsmöglichkeiten begrenzt waren, z. B. konnte ich eine Sammlung nicht auf eine andere Seite verweisen lassen, sondern es öffnete sich immer ein &amp;ldquo;Popup&amp;rdquo;, und das wollte ich einfach nicht. (Später habe ich herausgefunden, dass Remote-Layouts überschrieben werden können, aber da war es zu spät, denn ich hatte bereits zu einem anderen Theme-Anbieter gewechselt&amp;hellip;)&#10;Das Theme, das ich derzeit verwende, ist das &amp;ldquo;Minimal Mistakes Theme&amp;rdquo; (siehe Fußzeile der Website), das sehr gut dokumentiert ist. Nach dem Herunterladen und ein paar ersten Schritten kämpfte ich mit benutzerdefinierten Farben (_sass) und den Kollektionen - wieder einmal - da ich zunächst nicht verstand, wie Jekyll die Bindungen hinter den Kulissen durchführt. Einige meiner Schwierigkeiten sind in den Blog-Beiträgen vom Juni 2021 zu finden.&lt;/p&gt;&#10;&lt;h3 id="seitenlayouts-hero-screens"&gt;Seitenlayouts: Hero Screens&lt;/h3&gt;&#10;&lt;p&gt;Um die Navigation einfach und intuitiv zu gestalten, sollte man sich ein paar Gedanken darüber machen, wie das Layout der Seite aussehen soll. Die Herangehensweise wird sich bei jedem Seitentyp aufgrund der unterschiedlichen Komplexität und Dokumentstruktur unterscheiden. &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/hero_landing.jpg" alt="Image: landing hero"&gt;&lt;/figure&gt;&#10;Auf der Landing-/Homepage zum Beispiel soll die Navigation mitlaufen, so dass die Kategorien der Website immer sichtbar sind. Sie soll ein großes, breites Overlay-Bild mit der Seitenbeschreibung erhalten. Darunter sollen die neuesten Beiträge aufgelistet werden.&#10;Dies ist das Beispiel für die &amp;ldquo;Hero Pages&amp;rdquo; der Kollektion: &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/hero_collection.jpg" alt="Image: collection hero"&gt;&lt;/figure&gt;&#10;Die Projekte und ihre Inhalte enthalten eine Seitennavigation in der linken Seitenleiste, können ein scrollbares Panoramabild enthalten und haben eine breite Einstellung, um eine große Menge an Text im Inhaltsbereich anzuzeigen.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/hero_project.jpg" alt="Image: project hero"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Die &amp;ldquo;hero screen&amp;rdquo;-Skizzen für die Layouts dieser Website finden Sie &lt;a href="https://blog.schallbert.de/assets/docs/site_hero_screens.pdf"&gt;hier&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="sammlungen--kollektionen"&gt;Sammlungen / Kollektionen&lt;/h3&gt;&#10;&lt;p&gt;Sammlungen (englisch &amp;ldquo;Collections&amp;rdquo;) sind eine gute Möglichkeit, Inhalte zu gruppieren. Sehen Sie sich meine &lt;a href="https://blog.schallbert.de/projects/"&gt;Open Hardware Projekte&lt;/a&gt; Seite an. Sie können alle Projekte in dieser Kategorie mit einem kleinen Teaser-Bild und einer Beschreibung finden, um einen Überblick zu bekommen.&lt;/p&gt;&#10;&lt;p&gt;So kann die Konfiguration der Sammlung aussehen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Überlegen Sie sich einen passenden Namen für Ihre Sammlung, z.B. &lt;code&gt;electronics&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Der Name der Sammlung wird in &lt;code&gt;_config.yml&lt;/code&gt; als &lt;code&gt;collections: electronics&lt;/code&gt; definiert.&lt;/li&gt;&#10;&lt;li&gt;Der Sammlungsname muss in &lt;code&gt;#defaults&lt;/code&gt; als &lt;code&gt;type: electronics&lt;/code&gt; gesetzt werden, damit Jekyll weiß, wie der tatsächliche Inhalt zu interpretieren ist.&lt;/li&gt;&#10;&lt;li&gt;Eine Sammlungsseite muss erstellt werden (z.B. im Ordner &lt;code&gt;_pages&lt;/code&gt;) mit dem Attribut &lt;code&gt;collection: electronics&lt;/code&gt;. Dies ist die Seite, die die Übersicht der Artikel in der Sammlung anzeigt.&lt;/li&gt;&#10;&lt;li&gt;Es muss ein Ordner mit demselben Namen wie der Name der Sammlung mit führendem Unterstrich erstellt werden: &lt;code&gt;_electronics&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Alle Dokumente in diesem Ordner werden nach einem vollständigen Bauvorgang automatisch auf der Sammlungsseite angezeigt.&lt;/li&gt;&#10;&lt;li&gt;Diese Dokumente sollten ein &amp;ldquo;front matter&amp;rdquo; (Abschnitt vor dem eigentlichen Inhalt in der Datei, gekennzeichnet mit &lt;code&gt;---&lt;/code&gt;) erhalten, damit sie wie in der entsprechenden &lt;code&gt;_layouts&lt;/code&gt;-Datei beschrieben angezeigt werden können, z.B. &lt;code&gt;collection&lt;/code&gt;. Vergisst man dies, wirft einem Jekyll in der Konsole seitenlange, unverständliche Fehlermeldungen um die Ohren.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="mehr-als-das-standard-theme"&gt;Mehr als das Standard-Theme&lt;/h2&gt;&#10;&lt;p&gt;Ich habe zwar einige Theme-Standardeinstellungen überschrieben, verwende aber zumeist die bestehendenTheme-Infrastruktur. Dennoch hat meine Seite ein paar spezielle Anforderungen, so dass ich das Theme anpassen und neue Funktionen hinzufügen musste. Diese werden in den folgenden Abschnitten beschrieben.&lt;/p&gt;&#10;&lt;h3 id="navigation"&gt;Seitenleiste: Seitennavigation&lt;/h3&gt;&#10;&lt;p&gt;Da der Navigationsbaum meiner Website ziemlich flach ist und ich nicht viele Seiten habe, zu denen ich navigieren muss, wollte ich, dass die linke Seitenleiste die Seitennavigation und nicht die Website-Navigation enthält. Daher bräuchte ich keine rechte Seitenleiste, so dass mehr Platz für den Seiteninhalt zur Verfügung stünde.&lt;/p&gt;&#10;&lt;h3 id="inhaltsverzeichnis-auf-der-linken-seite"&gt;Inhaltsverzeichnis auf der linken Seite&lt;/h3&gt;&#10;&lt;p&gt;Wie &lt;a href="https://blog.schallbert.de/jekyll-toc/"&gt;hier&lt;/a&gt; und &lt;a href="https://blog.schallbert.de/jekyll-toc/"&gt;dort&lt;/a&gt; geschrieben, war ich nie 100%ig zufrieden damit, wie meine Projektseiten aussahen: &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-06-17_before_tocleft.jpg" alt="Image: Projektseite bevor ich toc_left einrichtete"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;Die ursprüngliche Idee des Themes ist, dass der Inhalt von zwei Seitenleisten eingerahmt wird. Die linke kann entweder Autoreninformationen, benutzerdefinierte Inhalte oder die Seitennavigation anzeigen. Der Inhaltsbereich ist zwischen breit und normal konfigurierbar, wobei in letzterem Fall Platz für eine zweite Seitenleiste auf der rechten Seite bleibt. Die rechte Seitenleiste ist so konfigurierbar, dass sie entweder gar nicht vorhanden ist oder das Inhaltsverzeichnis der Seite anzeigt.&lt;/p&gt;&#10;&lt;p&gt;Das Ziel war also die rechte Seitenleiste für mein Design zu eliminieren und statt der Seitennavigation in der linken Seitenleiste die Seitennavigation dorthin zu verschieben.&lt;/p&gt;&#10;&lt;h3 id="sidebar"&gt;Wo das Design der Seitenleiste definiert wird&lt;/h3&gt;&#10;&lt;p&gt;Es geht um den Ordner &lt;code&gt;/_layouts&lt;/code&gt;, in dem Informationen darüber gespeichert werden, wie ein Seitenlayout aussehen soll. Das fragliche Seitendesign heißt &lt;code&gt;single.html&lt;/code&gt; und ruft an einer bestimmten Stelle&#10;&lt;code&gt;include sidebar.html&lt;/code&gt; auf, das sich in &lt;code&gt;/_includes/sidebar.html&lt;/code&gt; befindet. Also habe ich mir einen neuen Parameter ausgedacht, mit dem ich die Anzeige des Inhaltsverzeichnisses in der linken Seitenleiste steuern kann, habe &lt;code&gt;toc_left&lt;/code&gt; gewählt und eine Bedingung hinzugefügt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;sidebar sticky&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- ...&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{.% if page.toc_left %}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; {.% include toc_left nav=page.toc_left %}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{.% endif %}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;... --&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(Trick: Ich musste &lt;a href="https://en.wikipedia.org/wiki/Jinja_%28template_engine%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;jinja&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; code auskommentieren und mit &lt;code&gt;...&lt;/code&gt; anführen, denn &lt;a href="https://jekyllrb.com/docs/liquid/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Liquid&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; würde sie sonst selbst als Kommandos erkennen)&lt;/p&gt;&#10;&lt;h3 id="implementierung-des-inhaltsverzeichnisses"&gt;Implementierung des Inhaltsverzeichnisses&lt;/h3&gt;&#10;&lt;p&gt;Es sollte in der linken Seitenleiste erscheinen. Also muste ich nur eine Datei namens &lt;strong&gt;toc_left&lt;/strong&gt; erzeugen, die den Generator für das Inhaltsverzeichnis wie folgt aufruft:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;aside&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;toc_left&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;nav&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;toc&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;markdown&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;header&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style="color:#f92672"&gt;h4&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;nav__title&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style="color:#f92672"&gt;i&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;fas fa-{{ include.icon | default: &amp;#39;file-alt&amp;#39; }}&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;i&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- ...&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{.{ include.title | .default: site.data.ui-text[site.locale].toc_label }}&amp;lt;/h4&amp;gt;&amp;lt;/header&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{.% include toc.html sanitize=true html=content h_min=1 h_max=6 class=&amp;#34;toc__menu&amp;#34; skip_no_ids=true %} &#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;... --&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;nav&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;aside&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="audio-einbettungen"&gt;Audio-Einbettungen&lt;/h3&gt;&#10;&lt;p&gt;Audio-Embeds sind diese kleinen Player-ähnlichen Dinger auf einer Website, die oft zu einer Streaming-Plattform oder ähnlichem weiterleiten. Ich brauche sie für einige meiner Audio-Projekte wie dieses &lt;a href="https://blog.schallbert.de/projects/mobfobamp/"&gt;mobfobamp&lt;/a&gt;: nur eine Überschrift und der Player darunter.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/embed-audio.jpg" alt="Image: embed-audio"&gt;&lt;/figure&gt;&#10;&lt;p&gt;&lt;a href="https://stackoverflow.com/a/63807971/13757172" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dieser Beitrag&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; hat mir sehr geholfen, so dass ich es einfach in das &lt;code&gt;_include&lt;/code&gt; einfügen konnte, es ein wenig modifizieren konnte, und es funktionierte auf Anhieb.&lt;/p&gt;&#10;&lt;h3 id="favicon"&gt;Platzierung des Favicons&lt;/h3&gt;&#10;&lt;p&gt;Ich folgte &lt;a href="https://ptc-it.de/add-favicon-to-mm-jekyll-site" target="_blank" rel="noopener noreferrer" class="external-link"&gt;dieser Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, um ein Favicon für die Website zu platzieren.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-06-13_favicon.jpg" alt="Image: favicon"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Der Pfad des Favicons muss unabhängig von dem Ordner, in dem sich die gerenderte Seite befindet, gültig sein. Ich musste &lt;code&gt;../&lt;/code&gt; entsprechend der Tiefe der Seiten in der Ordnerstruktur der Website hinzufügen, um sicherzustellen, dass das Favicon nicht nur auf der Startseite, sondern auch auf Unterseiten angezeigt werden kann.&lt;/p&gt;&#10;&lt;h3 id="optimierung-für-suchmaschinen"&gt;Optimierung für Suchmaschinen&lt;/h3&gt;&#10;&lt;p&gt;Da meine Website einige Wochen lang nicht von Suchmaschinen gefunden wurde, beschloss ich, eine so genannte &amp;ldquo;SEO&amp;rdquo; (Suchmaschinenoptimierung) durchzuführen. Deshalb fügte ich &lt;a href="http://jekyll.github.io/jekyll-seo-tag" target="_blank" rel="noopener noreferrer" class="external-link"&gt;jekyll-seo-tag&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; hinzu und überarbeitete z.B. alle meine Links nach &lt;a href="https://jsinibardy.com/optimize-seo-jekyll#seo-101" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Juliette Sinibardy&amp;rsquo;s SEO 101&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Danach habe ich meine Webseite dem Web-Analyse-Audit von Lighthouse unterzogen. &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/lighthouse_audit.jpg" alt="Image: Lighthouse Audit Ergebnisse"&gt;&lt;/figure&gt;&#10;Für einen ersten Versuch sieht es meiner Meinung nach ziemlich gut aus. Das größte Problem auf der Leistungsseite scheint der CSS-Overhead zu sein.&lt;/p&gt;&#10;&lt;h3 id="sicherheitsrichtlinie-für-inhalte"&gt;Sicherheitsrichtlinie für Inhalte&lt;/h3&gt;&#10;&lt;p&gt;Der Lighthouse-Audit hat gezeigt, dass es eine gute Idee wäre, externe Skripte - wie sie die Basisversion meines Themes enthält - einzuschränken. Also habe ich eine Content Security Policy (CSP) wie folgt eingeführt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;webrick&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;headers&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;Content-Security-Policy&lt;/span&gt;: &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;default-src &amp;#39;self&amp;#39;; &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;script-src &amp;#39;self&amp;#39;; &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;connect-src &amp;#39;self&amp;#39;; &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;img-src &amp;#39;self&amp;#39;; &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;style-src &amp;#39;self&amp;#39; &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;font-src &amp;#39;self;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mit dieser strikten Auslegung werden Inhalte Dritter gar nicht mehr geladen (aber leider meine eigenen teils auch nicht).&lt;/p&gt;&#10;&lt;p&gt;Anfänglich wurde das Hintergrundbild meiner Homepage nicht angezeigt, meine eingebetteten Audiodateien konnten nicht abgespielt werden und alle Icons, die von extern geladen wurden, waren ebenfalls weg. Das Hinzufügen von &lt;code&gt;media-src: 'self';&lt;/code&gt; und etwas mehr Lektüre in &lt;a href="https://www.jakobwillforss.com/post/content-security-policy-for-font-awesome-on-netlify" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Jakob Wilforss&amp;rsquo; Blog&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und vor allem &lt;a href="https://web.dev/strict-csp/?utm_source=lighthouse&amp;amp;utm_medium=lr" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Lighthouses eigene Vorschläge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; halfen bei der Lösung dieses Problems. Mit Hilfe der &lt;em&gt;Web developer Tools&lt;/em&gt; des Browsers habe ich die fehlenden Ressourcen ermittelt, die das Theme im Hintergrund zu laden versucht, so dass mein aktualisiertes CSP wie folgt aussieht:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;webrick&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;headers&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;Content-Security-Policy&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;default-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39; https://cdn.jsdelivr.net/; &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;script-src &amp;#39;self&amp;#39;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;connect-src &amp;#39;self&amp;#39;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;img-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39; https://upload.wikimedia.org/wikipedia/commons/ https://raw.githubusercontent.com;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;style-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39; https://cdn.jsdelivr.net/;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;font-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39; https://cdn.jsdelivr.net/; &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;media-src &amp;#39;self&amp;#39;; &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;object-src &amp;#39;none&amp;#39;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;base-uri &amp;#39;none&amp;#39;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: keine Drittanbieter mehr&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2021-12-05T00:00:00Z"&gt;&#10; 05. Dez. 2021&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Ende 2021 bin ich vom Nachladen von Inhalten über Drittanbieter (oben namentlich Font Awesome und Wikimedia) auf die eigene, statische Bereitstellung übergegangen. Somit gibt es keine Dritten mehr, die beim Aufruf meiner Seite etwas von Ihnen mitbekommen - und die Seite lädt etwas schneller. Meine Content Security Policy habe ich wieder auf die Obere der beiden Darstellungen zurückgedreht, &lt;code&gt;media-src 'self';&lt;/code&gt; aber beibehalten.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="fehler"&gt;Fehler&lt;/h2&gt;&#10;&lt;p&gt;Hier sammele ich Fehler, deren Lösung trivial erscheint, die mich aber dennoch ganze Abende gekostet haben.&lt;/p&gt;&#10;&lt;h3 id="jekyll-serve-utf-8-incompatible-character-encoding"&gt;Jekyll serve: &lt;code&gt;UTF-8&lt;/code&gt; incompatible character encoding&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/bundle_error.jpg" alt="Image: bundle error when front matter is missing"&gt;&lt;/figure&gt;&#10;Man gibt &lt;code&gt;bundle exec jekyll serve&lt;/code&gt; ein und erhält einen seltsamen &lt;code&gt;UTF-8&lt;/code&gt; &amp;ldquo;incompatible character encoding&amp;rdquo; Fehler? Die Ursache ist einfach: Eine der zu konvertierenden Dateien hat höchstwahrscheinlich keinen Header, der in das &amp;ldquo;Front matter&amp;rdquo; der Seite übersetzt werden kann, die aus dieser Datei generiert wird. Einfach die übliche Kopfzeile zur fraglichen Datei hinzufügen und schon funktioniert es wieder:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Feeds and Speeds database&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;description&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;for (light) CNC milling machines&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;header&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;blah/testimage.jpg&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="webseite-wird-nicht-dargestellt-404"&gt;Webseite wird nicht dargestellt: &lt;code&gt;404&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Dieser Fehler war besonders fies versteckt, denn auf meinem Rechner als &lt;code&gt;localhost&lt;/code&gt; wird die Seite einwandfrei dargestellt - nur &amp;ldquo;in Produktion&amp;rdquo; erscheint, wenn ich die Seite aufrufe, gar nichts.&lt;/p&gt;&#10;&lt;p&gt;Ursache: neuerdings verwende ich das &lt;strong&gt;permalink:&lt;/strong&gt; Attribut in der Frontmatter von Seiten, die übersetzt werden sollen und auf einer tieferen Ebene liegen - für diese möchte ich die Links kurz halten, daher bekommen sie manuell erstellte Permalinks. Nur wenn ich ihnen ein &amp;ldquo;trailing slash&amp;rdquo; gebe, lassen sie sich auch online aufrufen. Beispiel:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Falsch:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;This Page won&amp;#39;t be displayed&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;permalink&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/projects/pageerror&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Richtig:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;This Page will be displayed!&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;permalink&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/projects/nopageerror/&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Wird auf dem deutschen Blog zu:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="website-hosting"&gt;Website-Hosting&lt;/h2&gt;&#10;&lt;p&gt;Diese Seite ist selbst ein normales Gitea-Repository. Es wird automatisch mit einer von Github Actions entlehnten Aktion gebaut, sobald Änderungen zum &lt;code&gt;Main&lt;/code&gt; des Repositorys hinzugefügt werden.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Von Github Pages zu Gitea &amp;#43; Caddy&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-01-13T00:00:00Z"&gt;&#10; 13. Jan. 2024&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Inzwischen bin ich von Github-Pages nach Gitea &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/"&gt;umgezogen&lt;/a&gt; und hoste meine Webpräsenz nun selber. Dennoch sind alle Inhalte dieser Seite weiterhin als Repository verfügbar und können leicht von mir gewartet und auf Stand gebracht werden. Außerdem &lt;a href="https://blog.schallbert.de/server-protection/"&gt;schütze ich die Site nun vor unautorisiertem Zugriff&lt;/a&gt;.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="zusammenfassung"&gt;Zusammenfassung&lt;/h2&gt;&#10;&lt;p&gt;Sich damit vertraut zu machen wie Jekyll die Seite erstellt, wie das Theme mit meinen Befehlen interagiert und wie man das Verhalten des Themes überschreibt, hat einige Stunden gekostet. Aber ich denke, dass diese Seite eine klare Struktur und ein mehr oder weniger &amp;ldquo;professionelles&amp;rdquo; Aussehen besitzt - und ich musste nicht eine einzige Zeile JavaScript oder HTML schreiben, um das zu erreichen. Toll!&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Inhalt in Markdown &lt;code&gt;*.md&lt;/code&gt; geschrieben&lt;/li&gt;&#10;&lt;li&gt;Grundlegende Konfiguration der Seite in YAML &lt;code&gt;_config.yml&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Seitenlayouts in html und jinja &lt;code&gt;*.html&lt;/code&gt;, abgelegt in &lt;code&gt;_layouts&lt;/code&gt; und &lt;code&gt;_includes&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Seitenvariablen und Aussehen in SASS &lt;code&gt;*.scss&lt;/code&gt;, automatische Generierung von css-Dateien&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="lust-mitzumachen-oder-beiträge-beizusteuern"&gt;Lust mitzumachen oder Beiträge beizusteuern?&lt;/h2&gt;&#10;&lt;p&gt;Sie haben Ideen, Kritik oder interessante Dinge, die Sie mir mitteilen möchten? Nehmen Sie gern über meine &lt;a href="https://github.com/Schallbert/schallbert.github.io/discussions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Diskussions-Seite&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Kontakt auf.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Von Jekyll zu Hugo&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-09-06T00:00:00Z"&gt;&#10; 06. Sept. 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Im Sommer 2026 habe ich diese Webseite auf eine &lt;a href="https://blog.schallbert.de/announcements/2026-09-06-blog-anniversary-hugo/"&gt;neue technische Basis&lt;/a&gt; gestellt. Ich verwende jetzt Hugo als Site Generator. Eine Übersicht der Änderungen befindet sich in meinem &lt;a href="https://blog.schallbert.de/projects/migrating-jekyll-to-hugo/"&gt;Projekt über die Migration&lt;/a&gt;.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;</description></item><item><title>GoogleTest Demo</title><link>https://blog.schallbert.de/projects/platformio_gtestgmock/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/platformio_gtestgmock/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/build_test_success-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Googletest&amp;#39;s successful test results on the example program"&#10; title="GoogleTest Demo" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeitsgrad: Mittel 3/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: 0€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~2h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Es ist schön, bei der Entwicklung komplexerer Projekte ein leistungsfähiges Unit-Test-Framework zur Hand zu haben, das viel Zeit bei der Fehlersuche spart (vor allem das On-Target-Debugging in der Embedded-Welt kann sehr zeitaufwendig sein), die Codequalität verbessert und möglicherweise den Einblick in APIs und Architektur für andere verbessert.&lt;/p&gt;&#10;&lt;h2 id="die-welt-der-embedded-controller"&gt;Die Welt der &amp;ldquo;embedded controller&amp;rdquo;&lt;/h2&gt;&#10;&lt;p&gt;Traditionell waren Mikrocontroller auf der Speicherseite begrenzt. In der Vergangenheit hatten sie weder viel RAM noch PROGMEM, also war mein meist in schlankem &lt;code&gt;C&lt;/code&gt; geschrieben und die Konfiguration fand in Listen von &lt;code&gt;#define&lt;/code&gt; statt, die nicht viel Rechenzeit kosten, weil der Präprozessor die Arbeit machen würde und nicht der µC zur Laufzeit.&#10;Meine Unit-Tests waren immer an die Hardware gebunden, auf der sie liefen - nehmen Sie &lt;a href="https://github.com/ThrowTheSwitch/Unity" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Unity&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; als bekanntes Beispiel - aber jetzt wollte ich etwas Neues ausprobieren:&#10;Code objektorientiert schreiben und die Geschäftslogik unabhängig von der Hardware testen lassen. Ich wählte das &lt;a href="http://google.github.io/googletest" target="_blank" rel="noopener noreferrer" class="external-link"&gt;GoogleTest&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;-Framework und wollte eine nahtlose Integration in meinen Workflow mit &lt;a href="https://code.visualstudio.com" target="_blank" rel="noopener noreferrer" class="external-link"&gt;VScode&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und dessen Erweiterung &lt;a href="https://platformio.org" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PlatformIO&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erreichen.&lt;/p&gt;&#10;&lt;p&gt;Hierfür blieb bis vor kurzem nur der Weg, &lt;em&gt;GoogleTest&lt;/em&gt; per &lt;em&gt;GNU Compiler Collection&lt;/em&gt; parallel zu betreiben und durch PlatformIO ansteuern zu lassen. Neuerdings allerdings lässt sich &lt;em&gt;GoogleTest&lt;/em&gt; als &lt;a href="https://registry.platformio.org/libraries/google/googletest" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PlatformIO Library&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; direkt nutzen. Daher gehe ich hier auf beide Wege ein.&lt;/p&gt;&#10;&lt;h2 id="googletest-direkt-in-platformio-verwenden"&gt;GoogleTest direkt in PlatformIO verwenden&lt;/h2&gt;&#10;&lt;p&gt;Hier gestaltet sich die Installation besonders einfach:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Mein Github-Repository &lt;a href="https://github.com/Schallbert/PlatformIO_gTestgMock/tree/googletest-native" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PlatformIO-gTestgMock&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; klonen.&lt;/li&gt;&#10;&lt;li&gt;Im Repository-Ordner ein Konsolenfenster öffnen und per &lt;code&gt;git checkout googletest-native&lt;/code&gt; auf den entsprechenden Branch wechseln.&lt;/li&gt;&#10;&lt;li&gt;Den Anweisungen in der &lt;em&gt;README&lt;/em&gt;-Datei des Repository folgen und das Setup ausprobieren.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;So sieht es aus, wenn die Tests erfolgreich durchgeführt wurden:&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/native_test.jpg" alt="Image: googletest result success message in PlatformIO&amp;#39;s native environment"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h2 id="googletest-per-gcc-einsetzen"&gt;GoogleTest per GCC einsetzen&lt;/h2&gt;&#10;&lt;p&gt;Die benötigte Software kann man selbst (auch auf einem Windows&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; Rechner) installieren, um mit GoogleTest starten zu können.&lt;/p&gt;&#10;&lt;h3 id="mingw-herunterladen-und-installieren"&gt;MinGW herunterladen und installieren&lt;/h3&gt;&#10;&lt;p&gt;Siehe &lt;a href="https://community.platformio.org/t/unit-testing-with-gtest-gmock-on-env-desktop-on-arduino-platform/14354/7" target="_blank" rel="noopener noreferrer" class="external-link"&gt;diesen Thread&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; im Forum von PlatformIO, wenn es Fragen zu den folgenden Schritten gibt.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Unix-Benutzer: &lt;a href="https://blog.schallbert.de/projects/platformio_gtestgmock/#mingw-konfigurieren"&gt;bis hier&lt;/a&gt; die Schritte überspringen, denn wahrscheinlich werden nur die &lt;code&gt;libpthreadgc&lt;/code&gt;-Bibliotheken zusätzlich benötigt. Prüfen Sie das Vorhandensein aller Bibliotheken (aber lassen Sie das Präfix &lt;code&gt;mingw32-&lt;/code&gt; weg) und installieren ggf. fehlende nach. Stellen Sie sicher, dass die &lt;code&gt;PATH&lt;/code&gt;-Variable korrekt gesetzt ist.&lt;/li&gt;&#10;&lt;li&gt;Windows-Benutzer:&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Laden Sie MinGW herunter, z.B. von &lt;a href="https://sourceforge.net/projects/mingw" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Sourceforge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Installieren Sie durch Ausführen von &lt;code&gt;mingw-get-setup.exe&lt;/code&gt;. Wenn Sie grafische Benutzeroberflächen mögen, lassen Sie das entsprechende Kästchen angekreuzt. Ich empfehle &lt;em&gt;nicht&lt;/em&gt;, das Installationsverzeichnis zu ändern. Wenn Sie das doch tun, müssen Sie sich sich den neuen Pfad merken. &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/mingw_install.jpg" alt="MinGW install"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="mingw-konfigurieren"&gt;MinGW konfigurieren&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Damit GoogleTest läuft, müssen zusätzliche Pakete installiert werden:&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-gcc-g++&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-libmingwex&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-libmingwex-dll&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-libmingwex-dev&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-libpthreadgc-dll&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-libpthreadgc-dev&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Um diese zu installieren,&#10;&lt;ul&gt;&#10;&lt;li&gt;Wenn Sie die GUI verwenden: Suchen Sie die Bibliotheken unter &amp;ldquo;MinGW Standard Libraries&amp;rdquo; und fügen Sie sie nacheinander hinzu/aktivieren/installieren Sie sie.&lt;/li&gt;&#10;&lt;li&gt;Benutzer des Kommandozeilen-Interpreters: Geben Sie &lt;code&gt;mingw-get install&lt;/code&gt; ein und fügen Sie dann den Paketnamen hinzu.&#10;Wenn Sie eine Fehlermeldung erhalten, die besagt, dass einige der Pakete bereits existieren, umso besser.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Jetzt müssen Sie den Ordner &amp;ldquo;bin&amp;rdquo; von MinGW zu Ihren Systempfadvariablen hinzufügen, damit PlatformIO sie später finden kann. Wenn Sie ihn nicht von Hand modifiziert haben, sollte er &lt;code&gt;C:\MinGW\bin&lt;/code&gt; lauten.&#10;&lt;ul&gt;&#10;&lt;li&gt;GUI-Fans: folgen Sie &lt;a href="https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10" target="_blank" rel="noopener noreferrer" class="external-link"&gt;dieser Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;CLI: Geben Sie &lt;code&gt;set PATH=%PATH%;C:\MinGW\bin&lt;/code&gt; ein und überprüfen Sie, ob es mit &lt;code&gt;echo %PATH:;=&amp;amp;echo.%&lt;/code&gt; funktioniert.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Glückwunsch! Sie haben MinGW installiert, die Umgebungsvariablen so gesetzt, dass es von Drittanwendungen leichter gefunden wird, und es für die Zusammenarbeit mit GoogleTest konfiguriert.&lt;/p&gt;&#10;&lt;h3 id="platformio-konfigurieren"&gt;PlatformIO konfigurieren&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Nachdem die Variable &lt;code&gt;PATH&lt;/code&gt; gesetzt ist, muss VScode neu gestartet werden.&lt;/li&gt;&#10;&lt;li&gt;Damit PlatformIO korrekt mit dem gerade installierten Compiler verbunden wird, muss die &lt;code&gt;native&lt;/code&gt; Umgebung verwendet werden. Dort befindet sich der Compiler mit allen benötigten Umgebungsvariablen. Öffnen Sie ein PlatformIO-Terminal in einem Ihrer Projekte und geben Sie &lt;code&gt;pio platform install native&lt;/code&gt; ein. &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/install_native.jpg" alt="Image: how to install PIO native"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;li&gt;Zwei Möglichkeiten:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;A) Befolgen dieser Anleitung unter Verwendung meines Beispielprojektes wie &lt;a href="https://blog.schallbert.de/projects/platformio_gtestgmock/#writing-tests"&gt;unten&lt;/a&gt; beschrieben. Dann kann der nächste Abschnitt getrost übersprungen werden, da dieses Projekt vorkonfiguriert ist. Sie können nach Belieben damit herumspielen denn Sie verfügen von Anfang an über grundlegende Hardware-Abstraktionsschnittstellen.&lt;/li&gt;&#10;&lt;li&gt;B) Nehmen Sie Ihr eigenes Projekt und ändern Sie dessen &lt;code&gt;Platformio.ini&lt;/code&gt; so, dass GoogleTest ausgeführt werden kann.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ol start="4"&gt;&#10;&lt;li&gt;Dies sollten Sie zu Ihrer &lt;code&gt;Platformio.ini&lt;/code&gt; hinzufügen:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;env:desktop]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;platform = native&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;build_flags = &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;-&lt;span style="color:#ae81ff"&gt;std=gnu++11&lt;/span&gt; &lt;span style="color:#75715e"&gt;# use installed GNU C++11 compiler.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;-&lt;span style="color:#ae81ff"&gt;pthread&lt;/span&gt; &lt;span style="color:#75715e"&gt;# found in gtest documentation&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;lib_deps = &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;&lt;span style="color:#ae81ff"&gt;googletest&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Will automatically load latest googletest lib&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;lib_ignore = &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;src&lt;/span&gt; &lt;span style="color:#75715e"&gt;# most main.cpp&amp;#39;s directly access the hardware. &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Can be removed if your project is different.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;#&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# PLUS: Any files that contain bare-metal hardware commands &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# (e.g. digitalWrite), g++ won&amp;#39;t have the headers or specialized compiler knowledge!&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;lib_compat_mode = off&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Must-have for external stuff like gtest!&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Erklärung: Für die Umgebung &lt;code&gt;Desktop&lt;/code&gt; (= Computer ohne Microcontroller) wird der &lt;code&gt;native&lt;/code&gt; Compiler verwendet, d.h. GCC/G++, der oben in der Anleitung installiert und für den die &lt;code&gt;PATH&lt;/code&gt;-Variable festgelegt wurde. GoogleTest verwendet den C++11-Standard und Threading, der zu den Build-Flags hinzugefügt werden muss.&#10;Der Build von der &lt;code&gt;googletest&lt;/code&gt; Bibliothek ab. Es werden alle Dateien/Ordner ignoriert, die Quell- oder Headerdateien mit hardwarebezogenen Befehlen oder Headern enthalten. Der Bibliothekskompatibilitätsmodus muss ausgeschaltet sein, damit der Library Dependency Finder &lt;code&gt;googletest&lt;/code&gt; einbeziehen kann.&lt;/p&gt;&#10;&lt;ol start="5"&gt;&#10;&lt;li&gt;(Optional) Falls Sie lieber GUI-Schaltflächen verwenden als eine Zeile in die Konsole zu tippen um die Tests zu starten, erstellen Sie eine benutzerdefinierte &lt;a href="https://docs.platformio.org/en/latest/projectconf/build_configurations.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Konfiguration&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="tests-ausführen"&gt;Tests ausführen&lt;/h3&gt;&#10;&lt;p&gt;Für einen schnelleren Einstieg kann &lt;a href="https://github.com/Schallbert/PlatformIO_gTestgMock" target="_blank" rel="noopener noreferrer" class="external-link"&gt;mein Beispiel-Repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; geklont werden (den &lt;em&gt;main&lt;/em&gt; branch verwenden). Es enthält alle notwendigen Elemente, um zu prüfen, ob &lt;code&gt;gTest&lt;/code&gt; und &lt;code&gt;gMock&lt;/code&gt; korrekt funktionieren. So kann man direkt nachvollziehen, ob die Installation erfolgreich war.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Entpacken Sie die Zip-Datei bzw. verwenden Sie SSH, um das Repository zu klonen.&lt;/li&gt;&#10;&lt;li&gt;Öffnen Sie die Startseite der PlatformIO-Erweiterung, klicken Sie auf &amp;ldquo;Open Project&amp;rdquo;, und wählen Sie den Ordner aus.&lt;/li&gt;&#10;&lt;li&gt;Um die Unittests auszuführen, öffnen Sie ein PlatformIO-Terminal und geben Sie &lt;code&gt;pio test -vvv -e desktop -f test_desktop&lt;/code&gt; ein. (-vvv = wortreich -e = Umgebung, -f = Filter). Die Parameter sind erforderlich, weil die Tests nur auf der &amp;ldquo;Desktop&amp;rdquo;-Umgebung lauffähig sind, nicht aber auf dem Microcontroller.&lt;/li&gt;&#10;&lt;li&gt;Nach einiger Zeit sollte die unten dargestellte Meldung angezeigt werden.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/build_alltest_success.jpg" alt="successful build and test"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Super! Unit-Tests innerhalb von PlatformIO wurden gebaut und ausgeführt!&lt;/p&gt;&#10;&lt;h2 id="tipps-und-tricks"&gt;Tipps und Tricks&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Zu beachten ist&lt;/strong&gt;, dass der Ordner &lt;code&gt;PlatformIO_gTestgMock/.pio/build/desktop&lt;/code&gt; nun eine ausführbare Datei namens &lt;code&gt;program.exe&lt;/code&gt; enthält. Wird sie über die Kommandozeile ausgeführt, sollte das selbe Ergebnis wie in der obigen Abbildung herauskommen. Dies kann nützlich sein, wenn Tests nicht nur fehlschlagen, sondern das ganze Programm bei der Ausführung abstürzt. Zum Debuggen der Tests ist das auch hilfreich, da die CLI von PlatformIO dazu neigt, sich aufzuhängen. Man hat hier auch leichten Zugang zum Stracktrace - das sprengt den Rahmen dieser Übung aber bei Weitem.&lt;/p&gt;&#10;&lt;p&gt;Ab diesem Punkt&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;können Sie weiterlesen, um den Inhalt des Repositorys und die Funktionsweise von GoogleTest besser zu verstehen&lt;/li&gt;&#10;&lt;li&gt;Sie könnten das Schreiben von Tests üben, indem Sie weitere Fälle hinzufügen, z. B. um zu überprüfen, ob der Zustand &amp;ldquo;LOW&amp;rdquo; korrekt durchlaufen wird.&lt;/li&gt;&#10;&lt;li&gt;Sie könnten auch die Interfaces dieser Grundbausteine verbessern, z. B. durch Hinzufügen der Funktion &lt;code&gt;analogRead()&lt;/code&gt; zur Hardware-Abstraktionsschicht&lt;/li&gt;&#10;&lt;li&gt;Sie könnten Ihr eigenes Projekt darauf aufbauen, die Hardware-Abhängigkeiten sauber ausschalten und es so einfach auf verschiedene Hardware-Geräte und Hersteller portieren&lt;/li&gt;&#10;&lt;li&gt;Oder Sie beschäftigen sich weiter mit diesen anderen genialen Dingen, die Sie in die Tat umsetzen wollten, bevor Sie von diesem Tutorial abgelenkt wurden.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="repository-inhalte-erklärt"&gt;Repository-Inhalte erklärt&lt;/h2&gt;&#10;&lt;p&gt;Die folgenden Abschnitte behandeln die meisten Dateien im Repository und erklären, was sie im Detail tun. Die implementierte Geschäftslogik ist minimal und sollte als Beispielcode betrachtet werden. Ein reales Projekt, das alle auf dieser Seite dargestellten Muster verwendet, ist auf meiner &lt;a href="https://blog.schallbert.de/projects/tonuino/"&gt;Tonuino&lt;/a&gt;-Seite zu finden.&lt;/p&gt;&#10;&lt;h3 id="schnittstelle-der-hardware-abstraktionsschicht"&gt;Schnittstelle der Hardware-Abstraktionsschicht&lt;/h3&gt;&#10;&lt;p&gt;Schauen wir uns das Interface und seine Struktur genauer an.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// hal_if.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; hardwareAbstraction {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Hardware Abstraction Layer Interface class&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Hal_IF&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;virtual&lt;/span&gt; &lt;span style="color:#f92672"&gt;~&lt;/span&gt;Hal_IF() &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;virtual&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;digitalRead&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;virtual&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;digitalWrite&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId, &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; value) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#75715e"&gt;// hardwareAbstraction&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die Klasse &lt;code&gt;Hal_IF&lt;/code&gt; hat zwei Methoden, die beide &lt;code&gt;virtuell&lt;/code&gt; sind. Das bedeutet, dass eine Client-Klasse, die diese Schnittstelle verwendet, die Methode aufrufen kann und somit &amp;ldquo;automatisch&amp;rdquo; an die konkrete Implementierung der Methode weitergeleitet wird. Das &lt;code&gt;= 0;&lt;/code&gt; macht diese Methoden &lt;em&gt;rein virtuell&lt;/em&gt;, so dass das Überschreiben dieser Methoden durch eine abgeleitete Klasse nicht optional sondern vorgeschrieben ist. Das ist genau das, was wir hier wollen, denn wenn diese Methode nicht überschrieben würde, hätte sie kein Verhalten.&#10;Zu beachten ist, dass der Destruktor einer Schnittstellenklasse &lt;a href="https://stackoverflow.com/questions/3628529/should-c-interfaces-have-a-virtual-destructor" target="_blank" rel="noopener noreferrer" class="external-link"&gt;fast immer virtuell sein sollte&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="loopinout"&gt;LoopInOut&lt;/h3&gt;&#10;&lt;p&gt;Die zu testende Logik. Sie greift nicht direkt auf die Ein-/Ausgabe-Hardware des µC zu, sondern auf die Variable &lt;code&gt;m_hal&lt;/code&gt;, die eine Implementierung der Schnittstelle &lt;code&gt;Hal_IF&lt;/code&gt; ist. Sie wird bei der Objekterzeugung per Referenz (gekennzeichnet durch das &lt;code&gt;&amp;amp;&lt;/code&gt; bei der Typdeklaration) an die Klasse übergeben&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// loopInToOut.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;LoopInToOut&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; LoopInToOut(hardwareAbstraction&lt;span style="color:#f92672"&gt;::&lt;/span&gt;Hal_IF&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; hal)&lt;span style="color:#f92672"&gt;:&lt;/span&gt; m_hal(hal){};&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;~&lt;/span&gt;LoopInToOut() &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loopThrough&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; inPin, &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; outPin);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hardwareAbstraction&lt;span style="color:#f92672"&gt;::&lt;/span&gt;Hal_IF&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; m_hal;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dies ermöglicht die eigentliche Magie von gMock: Da der Client-Code mit einer Schnittstelle arbeitet, kann ich die &amp;ldquo;echte&amp;rdquo; Implementierung (des Hardware-Zugriffs) zur µC-Laufzeit übergeben, aber für den Test kann ich eine &amp;ldquo;Modell&amp;rdquo;-Implementierung übergeben, über die ich zum Zeitpunkt der Ausführung des Tests die Kontrolle habe.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// loopInToOut.cpp&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; LoopInToOut&lt;span style="color:#f92672"&gt;::&lt;/span&gt;loopThrough(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; inPin, &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; outPin)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; input &lt;span style="color:#f92672"&gt;=&lt;/span&gt; m_hal.digitalRead(inPin);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; m_hal.digitalWrite(outPin, input);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; input;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;loopThrough()&lt;/code&gt; ist wiederum sehr einfach. Sie liest den Zustand des Eingangspins und ordnet ihn dem Ausgangspin zu. Sie gibt den Zustand des Eingangspins an den Aufrufer zurück.&lt;/p&gt;&#10;&lt;h3 id="modellierte-hardwareabstraktion-mock-hal"&gt;Modellierte Hardwareabstraktion (mock hal)&lt;/h3&gt;&#10;&lt;p&gt;GoogleTest verwendet Makros, um Verhalten zu definieren. Die Mock-Klasse leitet sich ebenfalls von der Schnittstelle ab und stellt lediglich die Deklarationen der Schnittstellenmethoden bereit, die sie überschreibt. Es ist noch kein Verhalten festgelegt.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// hal_mock.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Hal_mock&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; hardwareAbstraction&lt;span style="color:#f92672"&gt;::&lt;/span&gt;Hal_IF&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; MOCK_METHOD(&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;, digitalRead, (&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId), (&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt;));&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; MOCK_METHOD(&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt;, digitalWrite, (&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId, &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; value), (&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt;));&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="die-testvorrichtung-test-fixture"&gt;Die Testvorrichtung (test fixture)&lt;/h4&gt;&#10;&lt;p&gt;Die Testvorrichtung enthält gemeinsame Daten und Verhaltensweisen für alle Testfälle innerhalb einer Testsuite. Name der Testsuite = Klassenname. Genau dieser Name muss in den Tests, die zu dieser Suite gehören sollen, wiederverwendet werden.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// unittest_loopInToOut.cpp&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loopInToOut_Test&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#f92672"&gt;::&lt;/span&gt;testing&lt;span style="color:#f92672"&gt;::&lt;/span&gt;Test&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;virtual&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; SetUp()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; m_loopInToOut &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; LoopInToOut(hal_mock);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;virtual&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TearDown&lt;/span&gt;()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;delete&lt;/span&gt; m_loopInToOut;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NiceMock&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Hal_mock&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; hal_mock{};&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; LoopInToOut&lt;span style="color:#f92672"&gt;*&lt;/span&gt; m_loopInToOut{&lt;span style="color:#66d9ef"&gt;nullptr&lt;/span&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Jeder Test verfügt also über einige vorbereitete Daten, die er verwenden kann: Die zu testende Klasse wird als Zeiger mit einer modellierten (Mock) Implementierung ihrer Abhängigkeit eingerichtet.&lt;/p&gt;&#10;&lt;h3 id="writing-tests"&gt;Der eigentliche Test&lt;/h3&gt;&#10;&lt;p&gt;Der Test wird durch das Makro &lt;code&gt;TEST_F&lt;/code&gt; definiert, was soviel bedeutet wie &amp;ldquo;Test mit Fixture&amp;rdquo;, das eine Testsuite und Testnamen erwartet.&#10;Das Makro &lt;code&gt;ON_CALL&lt;/code&gt; ist ein Befehl an gMock, der der modellierten Klasse vorgibt, wie sie sich zu verhalten hat, wenn &lt;code&gt;digitalRead(_)&lt;/code&gt; mit einer beliebigen Eingabe (Unterstrich = beliebig) aufgerufen wird: die Methode soll standardmäßig &lt;code&gt;true&lt;/code&gt; zurückgeben.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// unittest_loopInToOut.cpp&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;TEST_F(loopInToOut_Test, loop_inputHigh_writesCorrectOutputHigh)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ON_CALL(hal_mock, digitalRead(_)).WillByDefault(Return(true));&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; EXPECT_CALL(hal_mock, digitalWrite(&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, true));&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; m_loopInToOut&lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt;loopThrough(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dann wird eine &amp;ldquo;Erwartung&amp;rdquo; an die nachgebildete Klasse formuliert, die besagt, dass &lt;code&gt;digitalWrite&lt;/code&gt; mit Eingabewerten von &lt;code&gt;(2, true)&lt;/code&gt; aufgerufen wird, die ebenfalls auf Korrektheit geprüft werden.&lt;/p&gt;&#10;&lt;p&gt;Die letzte Anweisung ruft die echte Implementierung auf, so dass die Erwartungen überprüft werden können.&lt;/p&gt;&#10;&lt;h3 id="implementierung-des-hardwareverhaltens"&gt;Implementierung des Hardwareverhaltens&lt;/h3&gt;&#10;&lt;p&gt;Da &lt;code&gt;main.cpp&lt;/code&gt; auf die Hardware zugreift, muss es dafür eine Art Schnittstellenimplementierung geben.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// hal.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Hal&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Hal_IF&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Hal() &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;~&lt;/span&gt;Hal() &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;digitalRead&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;digitalWrite&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId, &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; value) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hier leitet &lt;code&gt;Hal&lt;/code&gt; sich von &lt;code&gt;Hal_IF&lt;/code&gt; ab und überschreibt alle seine Methoden.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// hal.cpp&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#include&lt;/span&gt; &lt;span style="color:#75715e"&gt;&amp;#34;Arduino.h&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#include&lt;/span&gt; &lt;span style="color:#75715e"&gt;&amp;#34;hal.h&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; hardwareAbstraction{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; Hal&lt;span style="color:#f92672"&gt;::&lt;/span&gt;digitalRead(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;digitalRead&lt;/span&gt;(pinId);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; Hal&lt;span style="color:#f92672"&gt;::&lt;/span&gt;digitalWrite(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId, &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; value) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; digitalWrite(pinId, value);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Man erkennt, dass nur diese Quelldatei tatsächlich die &lt;code&gt;Arduino.h&lt;/code&gt; für den Zugriff auf die Hardware einbindet. Die Abhängigkeit von den Hardware-Funktionen ist also sehr begrenzt und in Schnittstellen-Implementierungen gekapselt, die durch die Programmstruktur ausgewählt werden können. Während die &lt;code&gt;main.cpp&lt;/code&gt; für das Zielgerät also ein Objekt der obigen Implementierung erzeugt, wird der Test stattdessen ein Mock übergeben.&lt;/p&gt;&#10;&lt;h2 id="gtest-schlüsselkonzepte"&gt;Gtest Schlüsselkonzepte&lt;/h2&gt;&#10;&lt;p&gt;Googles offizielle &lt;a href="http://google.github.io/googletest/primer.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Webseite über Googletest&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kann sehr helfen, nicht nur für den Einstieg&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;. Das gmock Cookbook ist ein hervorragender Leitfaden, den ich im Laufe der Monate benutzt habe, als meine Tests immer komplexer und schwieriger zu schreiben wurden, und das gmock Cheat Sheet hilft, wenn man den Überblick verloren hat, wie ein Test für dieses oder jenes spezielle Problem zu schreiben ist.&lt;/p&gt;&#10;&lt;h3 id="dependency-injection"&gt;Dependency Injection&lt;/h3&gt;&#10;&lt;p&gt;Dependency Injection ist die grundlegende Mechanik, verstanden werden muss, damit man gMock erfolgreich anwenden kann. Ich versuche es mal mit einem Bild zu verdeutlichen.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/dependencyinjection.jpg" alt="Dependency Injection using interfaces"&gt;&lt;/figure&gt;&#10;&lt;p&gt;&lt;em&gt;Kurzgesagt ist die Schnittstelle lediglich ein Platzhalter für die Implementierung der Abhängigkeit der Client-Klassen.&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;Zur Laufzeit, z.B. wenn der Host das Client-Objekt erzeugt, wird er die Implementierung der Abhängigkeit an den Client übergeben (d.h. injizieren), der dann jede seiner Methoden aufrufen kann, da ihre Signaturen zur Kompilierzeit aufgrund der vom Client verwendeten Schnittstelle bekannt sind.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Das Mock ist nun lediglich eine Implementierung der Schnittstelle, die für die Tests spezialisiert ist.&lt;/li&gt;&#10;&lt;li&gt;Die eigentliche Implementierung wird in allen anderen Kontexten verwendet, sie zeigt das &amp;ldquo;echte&amp;rdquo; Verhalten dieser Klasse.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="hardware-abstraktionsschicht"&gt;Hardware-Abstraktionsschicht&lt;/h3&gt;&#10;&lt;p&gt;Dies ist vielleicht einer der kniffligen Punkte bei der Arbeit mit eingebetteter Software.&#10;Irgendwann geht es um die Interaktion mit der Außenwelt, sei es mit GPIO-Pins (General Purpose Input Output), den Zugriff auf eines der unzähligen Hardware-Bussysteme wie I2C, UART, SPI, CAN zur Steuerung externer Hardware oder einfach die Nutzung von Systeminterna wie dem EEPROM - da fangen die Probleme an.&lt;/p&gt;&#10;&lt;p&gt;Gtest läuft auf dem GCC/G++ Compiler, der einfach nichts von dem weiß, was der spezialisierte Compiler des jeweiligen Chips weiß. Er würde nicht einmal die Header-Dateien verstehen. Nun gibt es zwei Möglichkeiten, dieses Problem zu umgehen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&amp;ldquo;Fake Header&amp;rdquo; für jede µC-spezifische Implementierung schreiben, die von GCC/G++ verwendet werden können&lt;/li&gt;&#10;&lt;li&gt;Eine Schnittstelle für jede µC-spezifische Implementierung unter Verwendung einer (Hardware-)Abstraktionsschicht programmieren, so dass Ihre Produktionsumgebung die Chip-Hardware wie gewohnt verwendet, die Testumgebung aber stattdessen Mocks benutzt.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Der zweite Ansatz hat eine Reihe von Vorteilen:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Er ist sauber. Er zwingt einen dazu, keine Abstraktionsschichten zu zusammenzufassen die getrennt gehören.&lt;/li&gt;&#10;&lt;li&gt;Er ist einfach. Man kann die Geschäftslogik testen, ohne den µC zu programmieren! Es müssen nur die Eingänge gemockt und die Ausgänge abgefangen werden.&lt;/li&gt;&#10;&lt;li&gt;Er ist portabel. Wenn man die Implementierung Abstraktionsschicht austauscht, kann man sie auf einer anderen Controller-Familie oder sogar auf Chips eines anderen Herstellers auszuführen.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="nachteile"&gt;Nachteile&lt;/h3&gt;&#10;&lt;p&gt;Hmm, wo Licht ist, muss es doch irgendwo auch Schatten geben. Meiner Meinung nach sind die Nachteile, wenn man alles ohne angeschlossene Hardware testen will:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/memoryfull.jpg" alt="nicht viel Speicher übrig..."&gt;&lt;/figure&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Speicher. Sowohl ROM als auch RAM werden schneller voll, wenn eine große Anzahl von Klassen und vtables existieren.&lt;/li&gt;&#10;&lt;li&gt;Besitz. Die Injektion von Abhängigkeiten trennt den Besitz von der Nutzung eines Objekts, und man muss sich entscheiden, wie beides organisiert werden soll.&lt;/li&gt;&#10;&lt;li&gt;Bibliotheken von Drittanbietern können nicht einfach verwendet werden sondern benötigen Anpassung. Ein Interface-Header, eine &lt;a href="https://refactoring.guru/design-patterns/adapter/cpp/example" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Adapter-Implementierung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und ein Mock für jede Bibliothek, die &amp;ldquo;chip-internals&amp;rdquo; verwendet, sind zu erstellen.&lt;/li&gt;&#10;&lt;li&gt;Komplexität. Da Objekte über eine Schnittstelle injiziert werden, muss jetzt Call-by-Reference oder Übergabe per Zeiger durchgeführt werden 😨. Ersteres ist zusätzlich eingeschränkt, da es nur für Konstruktorinjektion funktioniert, nicht für andere Arten (Methodeninjektion, Setterinjektion usw.).&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="einige-weitere-tipps"&gt;Einige weitere Tipps&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Alles &amp;ldquo;hinter&amp;rdquo; der Abstraktionsschicht sollte so einfach wie möglich gehalten werden: Diese Dinge können nur auf dem Zielgerät getestet und ausgeführt werden. Beispiel: Jede Methode führt nur einen Befehl aus (wie in &lt;a href="https://github.com/Schallbert/PlatformIO_gTestgMock/blob/main/lib/hal/hal.cpp" target="_blank" rel="noopener noreferrer" class="external-link"&gt;meinem Beispielcode&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;).&lt;/li&gt;&#10;&lt;li&gt;Verwendung einer Loader-Klasse oder eines &lt;a href="https://www.fluentcpp.com/2019/06/07/write-your-own-dependency-injection-container" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dependency Injection Container (Nicolas Croad)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erleichtert die Arbeit, denn er enthält alle zu injizierenden Objekte und kann sie auf Anfrage übergeben.&lt;/li&gt;&#10;&lt;li&gt;Die &lt;a href="https://blog.schallbert.de/projects/platformio_gtestgmock/#nachteile"&gt;oben genannten Nachteile&lt;/a&gt; sollten im Blick behalten werden. Bei kleineren Projekten lohnt sich dieser Aufwand wahrscheinlich nicht.&lt;/li&gt;&#10;&lt;li&gt;Manchmal macht es sogar Sinn, mehrere Ebenen Schnittstellen zu haben. Dieses Muster kann nicht nur für die direkte Interaktion mit der Hardware zu verwendet werden, sondern auch zwischen Softwaremodulen, die dann in einer höheren Granularität getestet werden können. Dies kann sogar schneller sein als ein großen Haufen Klassen zu haben, die nur über Mocks in der Hardware-Abstraktionsschicht kontrolliert werden können.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="schwieriges-thema"&gt;Schwieriges Thema!&lt;/h3&gt;&#10;&lt;p&gt;Sie haben sich jetzt eine Pause verdient. Das waren viele Informationen, die Sie verarbeiten mussten. Nehmen Sie sich Zeit. Kontaktieren Sie mich gern auf der &lt;a href="https://github.com/Schallbert/PlatformIO_gTestgMock/discussions/1" target="_blank" rel="noopener noreferrer" class="external-link"&gt;discussions&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Seite zu diesem Thema, wenn Sie das Gefühl haben, dass etwas fehlt, nicht korrekt dargestellt ist, verbessert werden sollte oder einfach genau das ist, was Sie jetzt brauchten.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Die Installation unter Linux ist einfacher, da &lt;code&gt;gcc&lt;/code&gt; und &lt;code&gt;g++&lt;/code&gt; bereits vorinstalliert sind. Selbst wenn sie noch nicht die richtigen Pakete zur Unterstützung von GoogleTest enthalten, sind ihr Fehlermeldungen viel aussagekräftiger als die unter Windows, z.B. wenn Bibliotheken fehlen. 🙃&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;Dies wird auch &lt;a href="https://github.com/ninject/Ninject/wiki/Injection-Patterns" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Constructor Injection&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; genannt, eine Unterart der &lt;a href="https://github.com/ninject/ninject/wiki/Dependency-Injection-By-Hand" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dependency Injection&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die ein Mittel zur Erreichung der &lt;a href="https://en.wikipedia.org/wiki/Inversion_of_control" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Inversion Of Control&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist. Ich habe einige Links zu weiteren Informationen vorbereitet. Dies ist ein umfangreiches Thema, über das ganze Bücher geschrieben worden sind.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;Übrigens: die googletest-Seite ist auch statisch wie meine und verwendet ebenfalls Jekyll 👋 &amp;hellip;gefällt sie Ihnen? Ich würde gern Ihre Meinung dazu &lt;a href="https://github.com/Schallbert/schallbert.github.io/discussions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;auf meiner Diskussionsseite&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erfahren.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>QR-Codengrave</title><link>https://blog.schallbert.de/projects/qr-codengrave/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/qr-codengrave/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/app-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: qr-code created with my CNC portal milling machine"&#10; title="QR-Codengrave" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeitsgrad: Schwer 4/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: 0€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~50h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="die-idee"&gt;Die Idee&lt;/h2&gt;&#10;&lt;p&gt;Ein Freund bat mich, einen Bierdeckel für ihn anzufertigen. Ein eigenes Logo, ein paar Buchstaben. Nichts allzu Schweres. Auf der Rückseite wollte er einen &lt;a href="https://de.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener noreferrer" class="external-link"&gt;QR-Code&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; eingravieren lassen, der einen Link zu seiner Website enthält.&lt;/p&gt;&#10;&lt;p&gt;Ich habe also einen QR-Code mit einem &lt;a href="https://www.nayuki.io/page/qr-code-generator-library" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Standard-Webtool&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erstellt und als &lt;code&gt;.svg&lt;/code&gt; exportiert. Leider müsste ich in meinem CAM jedes einzelne Pixel anklicken und eine Carve-Action für dieses Pixel erstellen, was einige Minuten meiner Zeit für Auswahl-Klick-und-Parameter-Wähl-Prozeduren in Anspruch nehmen würde. Außerdem müsste ich einen Gravurstichel mit tiefenvariabler Gravur verwenden, um ein quadratisches Pixel zu erhalten, was viel mehr Bearbeitungszeit erfordert als eine runde Darstellung.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/svg_path_issues.jpg" alt="Bild: QR-Code SVG in meinem CAM"&gt;&lt;/figure&gt;&#10;&lt;p&gt;An diesem Punkt beschloss ich, viele, viele weitere Stunden meiner Zeit auf dieses Problem zu werfen und eine allgemeine Lösung für QR-Code-Gravuren zu entwickeln - eine Software namens &lt;strong&gt;QR-codengrave&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;h2 id="erste-schritte"&gt;Erste Schritte&lt;/h2&gt;&#10;&lt;h2 id="spezifikation"&gt;Spezifikation&lt;/h2&gt;&#10;&lt;p&gt;Das, was ich mit dem Tool erreichen wollte:&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Muss es können:&lt;/em&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Texteingabe verarbeiten und in einen QR-Code konvertieren. Der Benutzer muss keine zusätzliche Konfiguration vornehmen.&lt;/li&gt;&#10;&lt;li&gt;Bietet die Option zur Verwaltung verschiedener CNC-Werkzeuge.&lt;/li&gt;&#10;&lt;li&gt;Ermöglicht Eingabe von Gravurparametern: Gravurtiefe, Eilgangbewegung im Job, Überflughöhe&lt;/li&gt;&#10;&lt;li&gt;Ermöglicht Eingaben für benutzerdefinierte XY0-Offsets für Werkstücke&lt;/li&gt;&#10;&lt;li&gt;Vektorisiert die Pixelausgabe des QR-Codes&lt;/li&gt;&#10;&lt;li&gt;Kann Pfade aus den Vektoren erstellen lassen, beginnt der Einfachheit halber mit einer Spirale nach innen&lt;/li&gt;&#10;&lt;li&gt;Übergabe der Pixellinien an ein Modul, das die Parameter des ausgewählten Werkzeugs verwendet, um Maschinenbahnen zu erstellen&lt;/li&gt;&#10;&lt;li&gt;Maschinenbahnen in G-Code konvertieren, der später von meiner Maschine interpretiert werden kann.&lt;/li&gt;&#10;&lt;li&gt;Gründliche Überprüfung der Algorithmen mit automatisierten Unit-Tests (bitte keine Abstürze in der realen Welt)&lt;/li&gt;&#10;&lt;li&gt;Bietet eine grafische Benutzeroberfläche, um die Parametereingabe so einfach wie möglich zu machen.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;&lt;em&gt;Optional&lt;/em&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Eine Continuous-Integration-Pipeline sowohl für meine IDE als auch für Github-Aktionen erstellen&lt;/li&gt;&#10;&lt;li&gt;Eine Continuous-Deploy-Pipeline für meine IDE (PyInstaller) und Github-Aktionen (Artefakte, Releases) erstellen&lt;/li&gt;&#10;&lt;li&gt;Integrationstests für die GUI hinzufügen&lt;/li&gt;&#10;&lt;li&gt;Optimieren des Algorithmus zur Pfaderzeugung, da die &amp;ldquo;nach innen gerichtete Spirale&amp;rdquo; eine hohe Fertigungsqualität hat und schön anzusehen ist, aber nicht sehr effizient mit der Zeit umgeht.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="unbekannte"&gt;Unbekannte&lt;/h3&gt;&#10;&lt;p&gt;Zu Beginn dieses Projekts hatte ich einige offene Fragen und Risiken, die die Nutzbarkeit des Programms hätten einschränken oder sogar das gesamte Projekt undurchführbar machen können. Sie haben sich alle als falsch herausgestellt (zum Glück), aber dennoch sind hier die Risiken, die meine ganze Arbeit unbrauchbar hätten machen können:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Kameras lesen keine runden Punkt-als-Pixel-QR-Codes.&lt;/li&gt;&#10;&lt;li&gt;Der Kontrast, den meine Graviermaterialien bieten, ist zu gering, so dass das Lesen der Codes schlecht oder gar nicht funktioniert.&lt;/li&gt;&#10;&lt;li&gt;Die Bearbeitungszeit könnte sehr hoch sein, was das Projekt unwirtschaftlich machen würde.&lt;/li&gt;&#10;&lt;li&gt;Beschränkungen der Programmierwerkzeuge oder meiner Programmierfähigkeiten könnten die Entwicklungszeit so verlängern, dass es sich nicht lohnt, weitere Anstrengungen zu unternehmen.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Daher beschloss ich, &lt;a href="https://blog.schallbert.de/projects/qr-codengrave/#unit-tests"&gt;Unit-Tests&lt;/a&gt; zusammen mit dem Code zu schreiben, um ein schnelles Feedback darüber zu erhalten, ob mein Code das leisten kann, was er soll. Außerdem habe ich die Funktionalität des Programms auf eine Gravurstrategie beschränkt und die GUI so gestaltet, dass sie nur eine Vorschau des QR-Codes anzeigt, nicht aber XY-Offsets oder anderen Schnickschnack.&lt;/p&gt;&#10;&lt;h3 id="programmiersprache"&gt;Programmiersprache&lt;/h3&gt;&#10;&lt;p&gt;Ich nahm einige Blätter Papier und notierte mir das Wesentliche.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/sketches.jpg" alt="Bild: Softwareplanung auf Papier"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Ein Modul soll sich um die Erstellung eines QR-Codes aus einer Texteingabe kümmern, ein anderes soll Vektoren und Pfade erzeugen, um diesen QR-Code zu gravieren, und eine GUI in der Mitte soll die Texteingabe, eine Bearbeitungsvorschau und Eingabemöglichkeiten für Fräswerkzeuge, Gravurparameter und Werkstückversätze enthalten.&lt;/p&gt;&#10;&lt;p&gt;Ich hatte ein paar Programmiersprachen zur Auswahl, die ich gut genug &amp;ldquo;spreche&amp;rdquo;:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;C++ mit QT-Framework&lt;/li&gt;&#10;&lt;li&gt;Java&lt;/li&gt;&#10;&lt;li&gt;Python mit dem Tkinter-Framework&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Ich entschied mich für die - meiner Meinung nach - einfachste Lösung, um so schnell wie möglich einen lauffähigen Prototyp zu haben: Python mit Tkinter. Ich kenne Tkinter von einem meiner &lt;a href="https://github.com/Schallbert/DerWinterNaht" target="_blank" rel="noopener noreferrer" class="external-link"&gt;früheren Projekte&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und obwohl es seine Einschränkungen hat, ist es immer noch leistungsstark genug und schnell zu verwenden.&lt;/p&gt;&#10;&lt;h3 id="konzeptphase"&gt;Konzeptphase&lt;/h3&gt;&#10;&lt;p&gt;Nach etwas Recherche fand ich eine ziemlich gute Open-Source-&lt;a href="https://www.nayuki.io/page/qr-code-generator-library" target="_blank" rel="noopener noreferrer" class="external-link"&gt;QR-Code-Bibliothek im Internet&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die auch in Python verfügbar ist. Auf der Grundlage ihrer Ausgaben habe ich einen Vektorisierungsalgorithmus erstellt, der die QR-Code-Bibliothek abfragen kann, um die Werte der Pixel des QR-Codes in einer Scanzeile zurückzugeben:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;_qr_bitstream_from_line&lt;/span&gt;(self, line):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;Creates a bitstream from an input line of a QR-code data representation&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; :param line: a QR-code data representation (line of bits within the QR-code)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; :returns bitstream: returns an array of bits reflecting the QR code&amp;#39;s state at the respective&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; point of the line.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bitstream &lt;span style="color:#f92672"&gt;=&lt;/span&gt; []&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_direction() &lt;span style="color:#f92672"&gt;==&lt;/span&gt; Direction&lt;span style="color:#f92672"&gt;.&lt;/span&gt;RIGHT:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; x &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x, line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_end()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bitstream&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_qr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_module(x, abs(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y)))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_direction() &lt;span style="color:#f92672"&gt;==&lt;/span&gt; Direction&lt;span style="color:#f92672"&gt;.&lt;/span&gt;LEFT:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; x &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x, line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_end()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bitstream&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_qr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_module(x, abs(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y)))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_direction() &lt;span style="color:#f92672"&gt;==&lt;/span&gt; Direction&lt;span style="color:#f92672"&gt;.&lt;/span&gt;UP:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; y &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y, line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_end()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bitstream&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_qr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_module(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x, abs(y)))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_direction() &lt;span style="color:#f92672"&gt;==&lt;/span&gt; Direction&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DOWN:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; y &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y, line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_end()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bitstream&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_qr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_module(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x, abs(y)))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; bitstream&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dann musste ich aus diesen Linien Vektoren erstellen, so dass aufeinanderfolgende Pixel derselben Farbe zu einer Linie zusammengefügt werden konnten, die viel schneller herzustellen ist als einzelne Punkte:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;_vectorize_bitstream&lt;/span&gt;(self, bitstream):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;Creates a QrLineData object from an input bitstream&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; :param bitstream: an array of bits = a line of the QR-code&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; :returns line_vector: A list of QrLineData objects&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; line_vector &lt;span style="color:#f92672"&gt;=&lt;/span&gt; []&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data &lt;span style="color:#f92672"&gt;=&lt;/span&gt; QrLineData(bitstream[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;], &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; bit &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, len(bitstream)):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; bitstream[bit] &lt;span style="color:#f92672"&gt;==&lt;/span&gt; data&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_state():&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data&lt;span style="color:#f92672"&gt;.&lt;/span&gt;add_length()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; line_vector&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(data)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data &lt;span style="color:#f92672"&gt;=&lt;/span&gt; QrLineData(bitstream[bit])&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; data&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_state():&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data&lt;span style="color:#f92672"&gt;.&lt;/span&gt;finalize()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; line_vector&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(data)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; line_vector&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diese beiden Methoden würden nun von einer übergeordneten Methode aufgerufen werden, die einen spiralförmigen Pfad durch den QR-Code kennt.&#10;Meine erste GUI hatte nur drei Elemente: Ein Eingabefeld für den Text, der in einen QR-Code umgewandelt werden sollte, eine Schaltfläche, um den Algorithmus auszuführen, und eine Zeichenfläche, auf der der Gravurpfad des Algorithmus ausgegeben werden sollte.&lt;/p&gt;&#10;&lt;p&gt;Ich das Modul &lt;a href="https://docs.python.org/3/library/turtle.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Turtle&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; verwendet, um den vektorisierten QR-Code-Pfad zu zeichnen, ein Tool, das für Schülerinnen und Schüler geschrieben wurde, die sich in Programmiersprachen wie Python einarbeiten. Es ist zwar langsam, aber einfach einzurichten, so dass mir das für den Anfang genügte. Ich habe für&amp;rsquo;s Erste hart kodiert, um Zeit zu sparen, und meine Schleifen sind eher C-esk als pythonisch, aber egal.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; vect &lt;span style="color:#f92672"&gt;in&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_spiral_path[i]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_z_vector():&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_stop_draw:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;break&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; length &lt;span style="color:#f92672"&gt;=&lt;/span&gt; vect&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_length()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; vect&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_state():&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;turtle&lt;span style="color:#f92672"&gt;.&lt;/span&gt;down()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;turtle&lt;span style="color:#f92672"&gt;.&lt;/span&gt;forward(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pen_size &lt;span style="color:#f92672"&gt;*&lt;/span&gt; length)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;turtle&lt;span style="color:#f92672"&gt;.&lt;/span&gt;up()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;turtle&lt;span style="color:#f92672"&gt;.&lt;/span&gt;forward(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pen_size &lt;span style="color:#f92672"&gt;*&lt;/span&gt; length)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;turtle&lt;span style="color:#f92672"&gt;.&lt;/span&gt;right(&lt;span style="color:#ae81ff"&gt;90&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dieses sehr einfache Konzept sah für mich vielversprechend aus, aber ich musste ein paar Stunden daran feilen, weil meine anfängliche Ausgabe wie folgt aussah:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/qr-codengrave/drawing_issue_cut.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;QR-codengrave GUI test&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;Nach einer Weile konnte ich jedoch zuverlässig einen (21x21 Pixel) QR-Code auf den Bildschirm zeichnen und die Kamera meines Smartphones verwenden, um den Link zur Webseite zu erhalten. 🥳 Yeah!&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/qr-codengrave/working_scan.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;QR-codengrave Vektorerzeugung funktioniert&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="unit-tests"&gt;Unit-Tests&lt;/h3&gt;&#10;&lt;p&gt;Die &amp;ldquo;Plattform&amp;rdquo;-Teile - Klassen &lt;code&gt;vectorize_qr&lt;/code&gt; und &lt;code&gt;machinify_vector&lt;/code&gt; - enthalten beide eine Menge überprüfbarer Berechnungen und Geschäftslogik. Deshalb sind meine Tests mit Pythons Standardmodul &lt;a href="https://docs.python.org/3/library/unittest.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;unittest&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; schnell und einfach zu schreiben. In diesem Beispiel überprüfe ich, ob die von mir erstellte Linienklasse eine Länge (n+1 Stil) korrekt berechnet:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TestLine&lt;/span&gt;(unittest&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TestCase):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test_line_x_calculates_valid_length&lt;/span&gt;(self):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; line &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Line(Point(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;), Point(&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;assertEqual(&lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;, line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_abs_length())&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Aber es wurde schnell komplexer, als ich testen wollte, ob die Ausgabe der QR-Code-Generatorbibliothek in meinem Programm korrekt verarbeitet wird. Dazu musste ich die Bibliothek in meine zu testende Klasse einbinden, damit ich eine Mock-Implementierung einschleusen konnte, die dann im Test abgefragt wird.&lt;/p&gt;&#10;&lt;p&gt;Dieses Muster der Abhängigkeitsinjektion ist der Vorgehensweise in &lt;code&gt;C++&lt;/code&gt; sehr ähnlich, wo ich viel mehr Erfahrung habe (siehe ein Projekt &lt;a href="https://blog.schallbert.de/projects/platformio_gtestgmock/#dependency-injection"&gt;hier&lt;/a&gt; oder ein anderes &lt;a href="https://github.com/Schallbert/Tonuino_alternativeFirmware" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hier&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, zum Beispiel), nur dass in Python viel weniger Boilerplate-Code nötig ist, um dorthin zu gelangen. Ein paar Suchvorgänge im Internet und schon war der folgende Testfall geschrieben:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test_bitstream_qr_input_loop_xpos_correct&lt;/span&gt;(self):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mock_qr &lt;span style="color:#f92672"&gt;=&lt;/span&gt; QrCode&lt;span style="color:#f92672"&gt;.&lt;/span&gt;encode_text(&lt;span style="color:#e6db74"&gt;&amp;#34;schallbert.de&amp;#34;&lt;/span&gt;, QrCode&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Ecc&lt;span style="color:#f92672"&gt;.&lt;/span&gt;MEDIUM)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mock_qr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_module &lt;span style="color:#f92672"&gt;=&lt;/span&gt; MagicMock()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cam &lt;span style="color:#f92672"&gt;=&lt;/span&gt; VectorizeQr(mock_qr, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cam&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_qr_bitstream_from_line(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;test_line_x)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mock_qr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_module&lt;span style="color:#f92672"&gt;.&lt;/span&gt;assert_called_with(&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich injiziere die &amp;ldquo;echte&amp;rdquo; &lt;code&gt;QrCode&lt;/code&gt;-Bibliothek, aber mit einer Mock-Methode, die für mich von Interesse ist, &lt;code&gt;get_module()&lt;/code&gt;. In dem Test stelle ich sicher, dass die Methode mit den erwarteten Parametern aus meiner zu testenden Klasse aufgerufen wird.&lt;/p&gt;&#10;&lt;p&gt;Mit solchen Tests kann ich sichergehen, dass die Bausteine meines Algorithmus wie vorgesehen funktionieren und dass Regressionen schnell gefunden werden können.&lt;/p&gt;&#10;&lt;h2 id="der-lange-weg-zum-produkt-mit-minimalanforderungen"&gt;Der lange Weg zum Produkt mit Minimalanforderungen&lt;/h2&gt;&#10;&lt;p&gt;Beflügelt von diesem schnellen Erfolg, ging ich aufs Ganze. Vor der Implementierung des G-Code-Generators musste ich einen Werkzeugselektor entwerfen, da ich die Werkzeugeigenschaften zur Verfügung haben musste, bevor ich die Abmessungen und die Werkzeuggeschwindigkeit / den Vorschub für die QR-Code-Gravur einstellen konnte.&lt;/p&gt;&#10;&lt;p&gt;Nach einigen erfolglosen Versuchen im Hauptfenster entschied ich, dass das Werkzeugkonfigurationsfenster stattdessen ein Dialog sein sollte (den Tkinter &lt;a href="https://www.pythontutorial.net/tkinter/tkinter-toplevel/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Toplevel&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; nennt), so dass die Werkzeugparameter eingegeben und nach Abschluss an die Haupt-GUI zurückgeschickt werden können. Ich habe den Dialog modal gemacht mit&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_dialog&lt;span style="color:#f92672"&gt;.&lt;/span&gt;grab_set()&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So hat er immer den Fokus, damit nicht mehrere Instanzen dieses Fensters gleichzeitig vorhanden sein können.&#10;Ich habe die konfigurierten Werkzeuge in einer nummerierten Liste gespeichert, so dass die Anwendung, sobald sie gefüttert wurde, die Liste zur Verfügung stellt, damit der Benutzer das gewünschte Werkzeug für den Job aus einem Dropdown-Menü auswählen kann.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/tool_config.jpg" alt="Bild: QR-codengrave&amp;#39;s tool configuration dialog"&gt;&lt;/figure&gt;&#10;&lt;p&gt;In diesem Schritt musste ich zwei Dinge implementieren:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Überprüfungen für Wertetyp und -bereich&lt;/li&gt;&#10;&lt;li&gt;Ein Persistenzmodul, so dass die eingegebenen Tools einen Neustart der Anwendung überleben würden.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="überprüfungen-von-wertetyp-und--bereich"&gt;Überprüfungen von Wertetyp und -bereich&lt;/h3&gt;&#10;&lt;p&gt;Dies könnte mit einem Ereignis-Auslöser innerhalb des Eingabefeld-Widgets wie folgt erreicht werden:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; reg &lt;span style="color:#f92672"&gt;=&lt;/span&gt; config_tool_frame&lt;span style="color:#f92672"&gt;.&lt;/span&gt;register(validate_number)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tool_nr_entry&lt;span style="color:#f92672"&gt;.&lt;/span&gt;config(validate&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;key&amp;#34;&lt;/span&gt;, validatecommand&lt;span style="color:#f92672"&gt;=&lt;/span&gt;(reg, &lt;span style="color:#e6db74"&gt;&amp;#39;%P&amp;#39;&lt;/span&gt;))&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Anschließend wird der eingegebene Wert an einen einfachen Validator übergeben, der lediglich versucht, den Eingabewert in einen Float-Wert umzuwandeln und False zurückgibt, wenn dies einen Fehler erzeugt,&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;validate_number&lt;/span&gt;(entry):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;Helper function that is used in validators to check&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; that the entered keystroke is a number&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; entry &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; float(entry)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;except&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ValueError&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Eine Prüfung ob der Wert plausibel ist erfolgt erst, wenn die Schaltfläche &amp;ldquo;OK&amp;rdquo; gedrückt wird, um die eingegebenen Daten an die Anwendung weiterzuleiten. Bei Überschreitung des Bereichs würde ein Warn-Popup (Modul &lt;code&gt;tkinter.messagebox&lt;/code&gt;) erscheinen, das auf das mögliche Problem hinweist.&lt;/p&gt;&#10;&lt;h3 id="persistenz"&gt;Persistenz&lt;/h3&gt;&#10;&lt;p&gt;Das Persistenzmodul war etwas schwieriger zu implementieren.&#10;Ich entschied mich dafür, ein Element zu erstellen, das C++-Kenner eine &lt;em&gt;statische Klasse&lt;/em&gt; nennen würden, d.h. eine Klasse, die nicht instanziiert werden muss, um eine ihrer Methoden aufzurufen. Diese Klasse hat nur zwei Methoden namens &lt;code&gt;load&lt;/code&gt; und &lt;code&gt;save&lt;/code&gt;, die das tun, was ihr Name andeutet, indem sie Pythons &lt;a href="https://docs.python.org/3/library/pickle.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;pickle&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Modul zur Serialisierung von Objekten verwenden, die ich speichern wollte. In Python habe ich den Dekorator &lt;code&gt;@classmethod&lt;/code&gt; verwendet, um die statische Natur dieser Klasse zu kennzeichnen.&lt;/p&gt;&#10;&lt;p&gt;So einfach kann die Serialisierung von Daten mit den richtigen Werkzeugen werden:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; open(app_persistence_path, &lt;span style="color:#e6db74"&gt;&amp;#39;wb&amp;#39;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; file:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pickle&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dump([cls&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_tool_list,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cls&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_z_params,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cls&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_xy0],&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; file, protocol&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Als dies zuverlässig funktionierte, fügte ich ein Widget zur Konfiguration der Gravurparameter hinzu. Von Nachteil bei statischen Methoden ist, dass sie nur schlecht unit-testbar sind, weil Dependency Injection hier nicht funktioniert; sie sind ja instanzlos. Es bliebe mir nichts anderes üblich als &amp;ldquo;Fakes&amp;rdquo; zu schreiben, die im Testfalle die echte Speicher- bzw. Ladefunktion ersetzt.&lt;/p&gt;&#10;&lt;h3 id="gravur-parameter"&gt;Gravur-Parameter&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/engrave_params.jpg" alt="Bild: QR-codengrave&amp;#39;s engrave parameter configuration dialog"&gt;&lt;/figure&gt;&#10;Um den G-Code aus meinen QR-Code-Daten zu generieren, benötigte ich nicht nur Werkzeug- und Geschwindigkeits-/Vorschubdaten, sondern ich musste auch wissen, wie tief ich gravieren wollte, wie weit das Werkzeug bei Eilgängen über der Werkstückoberfläche schweben sollte und welche Sicherheitshöhe die CNC für die Rückkehr zum Ursprung oder zur Ausgangsposition verwenden sollte.&lt;/p&gt;&#10;&lt;p&gt;Es wird auf ähnliche Weise wie das Dialogfeld &amp;ldquo;Werkzeugkonfiguration&amp;rdquo; erstellt und verwendet dieselben Mechanismen zur Überprüfung von Typ und Bereich. Der einzige Unterschied besteht darin, dass er nicht über einen Klick auf eine Schaltfläche, sondern direkt über das zugehörige Textfeld gestartet wird, das den aktuellen Wert der Gravurparameter im Hauptfenster anzeigt.&lt;/p&gt;&#10;&lt;h3 id="werkstück-xy-nullpunktverschiebung"&gt;Werkstück XY Nullpunktverschiebung&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/xy0_config.jpg" alt="Bild: QR-codengrave&amp;#39;s Werkstückversatz-Konfigurationsdialog"&gt;&lt;/figure&gt;&#10;Der Konfigurationsdialog für den Werkstückversatz enthält einige Radiobuttons zur Auswahl der gängigsten Werkstückursprünge und eine benutzerdefinierte Option, mit der der Benutzer Werte für den X- und Y-Achsennullpunkt des Werkstücks eingeben kann.&#10;Eine Funktion im Hintergrund berechnet den erforderlichen Versatz relativ zum Startpunkt der Gravierspirale.&lt;/p&gt;&#10;&lt;p&gt;Normalerweise würde ich nicht wollen, dass die grafische Benutzeroberfläche Berechnungen durchführt oder viel Verzweigungslogik verwendet, da dies schwieriger zu verifizieren ist, weil die Test Fixtures damit größer und schwieriger zu warten werden. In diesem Fall habe ich mich jedoch entschieden, die Offset-Berechnung mit Hilfe einer Wertetabelle innerhalb des GUI-Moduls durchzuführen, um zu vermeiden, dass alle GUI-Module zur Plattform und zurück kommunizieren müssen (und das durch 4 Klassen hindurch).&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;_get_xy_offset&lt;/span&gt;(self, offset):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;calculates an XY coordinate offset from a preset point, taking the selected tool diameter into account.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; :param offset the selected offset from the above enum class&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; :returns a XY Point where XY0 is assumed for the engraving.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; qr &lt;span style="color:#f92672"&gt;=&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_qr_dimension&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; d &lt;span style="color:#f92672"&gt;=&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_tool_diameter&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; offsets &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {Offset&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CENTER: Point((d &lt;span style="color:#f92672"&gt;-&lt;/span&gt; qr) &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, (qr &lt;span style="color:#f92672"&gt;-&lt;/span&gt; d) &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Offset&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TOPLEFT: Point(d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Offset&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TOPRIGHT: Point(d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; qr, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Offset&lt;span style="color:#f92672"&gt;.&lt;/span&gt;BOTTOMLEFT: Point(d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, qr &lt;span style="color:#f92672"&gt;-&lt;/span&gt; d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Offset&lt;span style="color:#f92672"&gt;.&lt;/span&gt;BOTTOMRIGHT: Point(d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; qr, qr &lt;span style="color:#f92672"&gt;-&lt;/span&gt; d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; offset &lt;span style="color:#f92672"&gt;in&lt;/span&gt; offsets:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; offsets[offset]&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="erstes-release-v10"&gt;Erstes Release V1.0&lt;/h2&gt;&#10;&lt;p&gt;Die erste Version ist minimalistisch. Sie erfüllt ihren Zweck, hat aber noch einige Fehler (z.B. eine Fehlermeldung, wenn kein Werkzeug in der Auswahlliste ausgewählt ist) und Unzulänglichkeiten (z.B. ist das Zeichnen eines QR-Codes sehr langsam, beim erneuten Zeichnen kippt der QR-Code ein wenig usw.), aber sie sollte einfach funktionieren und dem Benutzer helfen, die schlimmsten Fehler auf seinem Weg zu vermeiden.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/main_application.jpg" alt="Bild: Hauptbildschirm von QR-codengrave"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Ich habe die Ausführung auf meiner CNC-Maschine getestet und zumindest mein Handy ist in der Lage, zu einer Website zu navigieren, indem es einfach die Kamera auf das Werkstück hält 📱.&lt;/p&gt;&#10;&lt;h2 id="automatisierte-integrationstests-für-die-gui-frustrierende-arbeit"&gt;Automatisierte Integrationstests für die GUI: Frustrierende Arbeit&lt;/h2&gt;&#10;&lt;p&gt;Als meine erste Version herauskam, wollte ich einige Tests für die grafische Benutzeroberfläche hinzufügen. Da ich es nicht geschafft habe, dort absolut keine Geschäftslogik zu haben, wollte ich zumindest Bereichsprüfungen sowie die Kommunikation mit der Plattform überprüfen, um sicherzustellen, dass alles wie vorgesehen funktioniert.&lt;/p&gt;&#10;&lt;h3 id="aufgehängt-in-der-messagebox"&gt;Aufgehängt in der Messagebox&lt;/h3&gt;&#10;&lt;p&gt;Zu diesem Zweck schrieb ich einige Tests, die auf meinem lokalen Rechner liefen, mit dem Nachteil, dass, wenn der Test auf einen Warn- oder Fehlerdialog &lt;code&gt;messagebox.showinfo&lt;/code&gt; / &lt;code&gt;messagebox.showerror&lt;/code&gt; stieß, eine manuelle Benutzerinteraktion unbedingt erforderlich war, damit das Fenster geschlossen und der Test fortgesetzt werden konnte.&lt;/p&gt;&#10;&lt;p&gt;Ich habe keine einfache Möglichkeit gefunden, dieses Problem zu umgehen, da die Messagebox als unabhängiges Widget agiert, das nicht einfach manipuliert werden kann. Auch meine Versuche, einen Tastendruck als Seiteneffekt aus dem Test heraus auszulösen, waren nicht von Erfolg gekrönt (und wären am Ende wohl auch keine saubere Lösung gewesen).&lt;/p&gt;&#10;&lt;p&gt;Meine Lösung nach ein paar Stunden gemurmelten Fluchens ist, dass die Messagebox-Widgets in die GUI-Klassen injiziert werden, die sie verwenden:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@patch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;src.gui.gui_tool_manage.GuiToolManager&amp;#39;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;setUp&lt;/span&gt;(self, mock_guitoolmanager):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Tk() &lt;span style="color:#75715e"&gt;# required to have tk variables properly instantiated&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mock_guitoolmanager &lt;span style="color:#f92672"&gt;=&lt;/span&gt; mock_guitoolmanager&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mock_msg &lt;span style="color:#f92672"&gt;=&lt;/span&gt; MsgBox()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mock_msg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;showinfo &lt;span style="color:#f92672"&gt;=&lt;/span&gt; MagicMock()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Persistence&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_mock_msgbox(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mock_msg)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;config_tool &lt;span style="color:#f92672"&gt;=&lt;/span&gt; GuiConfigureTool(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mock_guitoolmanager, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mock_msg, {&lt;span style="color:#e6db74"&gt;&amp;#39;padx&amp;#39;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;pady&amp;#39;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;})&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Auf diese Weise kann ich &lt;code&gt;mock_msg&lt;/code&gt; in die Gui-Klasse werfen, so dass nicht die echte showinfo genommen wird, sondern das Mock weiß, ob er wie vorgesehen aufgerufen wurde. Ich könnte ihr zur Testvorrichtung hinzufügen, so dass mein Testcode selbst &lt;a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself" target="_blank" rel="noopener noreferrer" class="external-link"&gt;DRY&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bleiben könnte.&lt;/p&gt;&#10;&lt;p&gt;Es fügt dem Konstruktor mehr Argumente hinzu und delegiert die Instanziierung an eine Klasse höherer Ebene, aber die Verwendung innerhalb der zu testenden Klasse bleibt trivial. Im Einsatz unter Realbedingungen agiert die Schnittstelle wie ein Proxy, der dann die echte &lt;code&gt;showinfo&lt;/code&gt;-Methode des Messagebox Objektes aufruft.&lt;/p&gt;&#10;&lt;h3 id="probleme-mit-statischen-klassen"&gt;Probleme mit statischen Klassen&lt;/h3&gt;&#10;&lt;p&gt;Da die Persistenzklasse, die ich &lt;a href="https://blog.schallbert.de/projects/qr-codengrave/#persistenz"&gt;oben&lt;/a&gt; erwähnt habe, &lt;em&gt;statisch&lt;/em&gt; ist, d.h. keine Instanziierung erfordert, hatte ich das nächste Problem von Anfang an in meinen Code eingebaut: Ein Integrationstest würde nun versuchen, in eine tatsächliche &amp;ldquo;persistence.dat&amp;rdquo;-Datei zu schreiben, eine Abhängigkeit, die ich nicht haben wollte, insbesondere nicht in der kontinuierlichen Integration (CI) - denn dort würde das Programm die Rechte für das Schreiben auf Festplatte nicht besitzen.&lt;/p&gt;&#10;&lt;p&gt;Also habe ich Persistence in die Haupt-GUI-Datei verschoben, die nicht getestet werden würde, und folgte damit &lt;a href="https://blog.stevensanderson.com/2009/11/04/selective-unit-testing-costs-and-benefits/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Steven Andersons großartigem Blogbeitrag&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Die Haupt-GUI sollte ohnehin nur deklarativen Code enthalten, und die geladenen Daten werden dann in die Klassen kaskadiert, die sie verwenden, anstatt in die Persistenzklasse, die die eigentliche Lade-/Speicheroperation durchführt.&lt;/p&gt;&#10;&lt;h2 id="zukünftige-versionen"&gt;Zukünftige Versionen&lt;/h2&gt;&#10;&lt;p&gt;Folgendes ist für spätere Releases geplant:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Die Zeichnungsgeschwindigkeit des QR-Codes muss erhöht werden&lt;/li&gt;&#10;&lt;li&gt;Grafikfehler (Skalierung, Kippen) und unscharfe Ränder des Zeichenbildschirms sollen behoben werden&lt;/li&gt;&#10;&lt;li&gt;CNC-Bahnen sollen optimiert werden, um die Fertigungszeit zu verringern&lt;/li&gt;&#10;&lt;li&gt;Fehlerbehebungen&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Zumindest wenn es welche gibt; mal sehen, wie oft ich dieses Tool benutzen werde. Die Dinge, die mich am meisten stören, werden wahrscheinlich zuerst behoben werden.&lt;/p&gt;&#10;&lt;p&gt;Ach ja, noch etwas: Danke für&amp;rsquo;s Lesen des gesamten Artikels. Zur Belohnung gibt es eine Kopie &lt;a href="https://github.com/Schallbert/QR-codengrave/releases/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;QR-codengrave&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kostenlos zum Herunterladen. Es kann für den privaten Gebrauch gern verwendet werden. Falls Sie eine kommerzielle Nutzung planen, lassen Sie es mich bitte im Voraus wissen.&lt;/p&gt;&#10;</description></item><item><title>Tonuino - alternative Firmware</title><link>https://blog.schallbert.de/projects/tonuino/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/tonuino/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/tonuino-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Tonuino as a Bird&amp;#39;s house"&#10; title="Tonuino - alternative Firmware" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: Experte 5/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: 0€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~400h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="galerie"&gt;Galerie&lt;/h2&gt;&#10;&lt;section class="hugo-gallery"&gt;&#10; &lt;div class="hugo-gallery__frame"&gt;&#10; &lt;div class="hugo-gallery__grid" role="list"&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-0"&#10; aria-label="Image: NFC tags have been woven into little birds made of felt. They can be positioned inside the bird&amp;#39;s house to play music."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_front.jpg"&#10; alt="Image: NFC tags have been woven into little birds made of felt. They can be positioned inside the bird&amp;#39;s house to play music."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-1"&#10; aria-label="Image: birdshouse opened. Its contents: An RFID reader, MP3 player, speaker, charger socket, a powerbank, one multicolor-LED and the clickEncoder as human-machine interface."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_inside.jpg"&#10; alt="Image: birdshouse opened. Its contents: An RFID reader, MP3 player, speaker, charger socket, a powerbank, one multicolor-LED and the clickEncoder as human-machine interface."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-2"&#10; aria-label="Image: Guitar Amp design: All components are built in an old guitar amp model. Its battery, amplifier and speaker have been reused for Tonuino, and some buttons have been added to control the mp3 player. Cards are placed on top of the amp to be read and select content to be played."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_top.jpg"&#10; alt="Image: Guitar Amp design: All components are built in an old guitar amp model. Its battery, amplifier and speaker have been reused for Tonuino, and some buttons have been added to control the mp3 player. Cards are placed on top of the amp to be read and select content to be played."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-3"&#10; aria-label="Image: insides of the guitar amp: An Arduino nano controls all functions. A Bistable relay keeps the system alive as long as it plays music and switches off automatically when done to save battery power."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_inside.jpg"&#10; alt="Image: insides of the guitar amp: An Arduino nano controls all functions. A Bistable relay keeps the system alive as long as it plays music and switches off automatically when done to save battery power."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;&#10; Die alternative Tonuino-Firmware im Einsatz.&#10; &lt;/div&gt;&lt;/div&gt;&#10;&#10; &lt;div class="hugo-gallery__full" aria-live="polite"&gt;&lt;figure&#10; id="gallery-full-0"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_front.jpg"&#10; alt="Image: NFC tags have been woven into little birds made of felt. They can be positioned inside the bird&amp;#39;s house to play music."&gt;&lt;figcaption&gt;NFC-Tags sind Vögel, die zum Abspielen ins Häuschen gesetzt werden&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-1"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_inside.jpg"&#10; alt="Image: birdshouse opened. Its contents: An RFID reader, MP3 player, speaker, charger socket, a powerbank, one multicolor-LED and the clickEncoder as human-machine interface."&gt;&lt;figcaption&gt;Anwendungsbeispiel mit Microcontroller unter Verwendung meiner ClickEncoder-Bibliothek&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-2"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_top.jpg"&#10; alt="Image: Guitar Amp design: All components are built in an old guitar amp model. Its battery, amplifier and speaker have been reused for Tonuino, and some buttons have been added to control the mp3 player. Cards are placed on top of the amp to be read and select content to be played."&gt;&lt;figcaption&gt;Anwendungsbeispiel mit Arduino in einem Gitarrenverstärker&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-3"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_inside.jpg"&#10; alt="Image: insides of the guitar amp: An Arduino nano controls all functions. A Bistable relay keeps the system alive as long as it plays music and switches off automatically when done to save battery power."&gt;&lt;figcaption&gt;Gesteuert wird der Player in diesem Fall mit drei Tasten.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://raw.githubusercontent.com/isocpp/logos/64ef037049f87ac74875dbe72695e59118b52186/cpp_logo.svg" alt="Image: C&amp;#43;&amp;#43; Logo"&gt;&lt;/figure&gt;&#10;In disem Projekt entstand eine Software für eine NFC-Tag gesteuerte Jukebox. Sie ist für das Arduino-Framework optimiert und verbindet die Hardware eines Mp3-Players (mit SD-Karte), ein NFC-Tag-Lesegerät und ein paar Bedienelemente.&lt;/p&gt;&#10;&lt;p&gt;Ein NFC-Tag wird mit einem Ordner auf der SD-Karte verknüpft, welcher Audiodateien enthält. Das Tag speichert auch Informationen über den gewünschten Wiedergabemodus für dieses Album, z. B. &amp;ldquo;Zufällig&amp;rdquo;. Sobald ein verknüpfter Tag in die Nähe der Jukebox gebracht wird, wird der konfigurierte Ordner im entsprechenden Wiedergabemodus abgespielt.&#10;Mit den Bedienelementen kann der Titel ausgewählt, die Lautstärke geändert und sogar durch ein Sprachmenü navigiert werden, was das Löschen, Verknüpfen und Konfigurieren eines Tags sowie Sperren/Entsperren der Eingabetasten ermöglicht.&lt;/p&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Idee und fantastische Umsetzung hatte ich auf &lt;a href="https://www.voss.earth/tonuino" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Thorsten Voß&amp;rsquo; blog&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gesehen. Es begann mit der Funktionalität (&lt;a href="https://blog.schallbert.de/projects/encoder/"&gt;encoder support&lt;/a&gt;), die ich hinzufügen wollte. Leider war der ursprüngliche Code ein zusammenhängender Block mit mehreren tausend Zeilen Code und für mich schlechter Lesbarkeit, so dass es mir schwer fiel, meine Änderungen einzubringen.&lt;/p&gt;&#10;&lt;p&gt;Also habe ich den Code von Grund auf neu geschrieben, um eine bessere Struktur, Lesbarkeit, Wartbarkeit und Erweiterbarkeit zu erreichen. Parallel dazu las ich einige Bücher über objektorientiertes Design in &lt;code&gt;C++&lt;/code&gt;, &lt;a href="https://en.wikipedia.org/wiki/Robert_C._Martin" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Clean Code&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und &lt;a href="https://en.wikipedia.org/wiki/Software_design_pattern" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Software Design Patterns&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und wandte einige davon dort an, wo ich sie für geeignet hielt. Auf diese Weise konnte ich sowohl die Modularität als auch die Lesbarkeit des Codes verbessern und habe außerdem gelernt, wie man Code in einem &amp;ldquo;größeren&amp;rdquo; Projekt schreibt.&lt;/p&gt;&#10;&lt;h2 id="schwierigkeiten"&gt;Schwierigkeiten&lt;/h2&gt;&#10;&lt;p&gt;Dies war mein erstes eigenes &lt;code&gt;C++&lt;/code&gt;-Projekt, und vielleicht war es für den Anfang ein wenig zu groß. All die für mich neuen Konzepte, das Schreiben an eine Schnittstelle, die erstmalige Verwendung von &lt;a href="https://platformio.org" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Platformio als IDE&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die Verwendung des &lt;a href="https://google.github.io/googletest" target="_blank" rel="noopener noreferrer" class="external-link"&gt;googletest unit test framework&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die Einsatz von Coding Patterns wie [factory](https://www. oodesign.com/factory-pattern.html) oder &lt;a href="https://en.wikipedia.org/wiki/Dependency_injection" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dependency Injection&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kostete mich Abend um Abend über fast ein Jahr, bis ich dieses Projekt schließlich fertigstellen konnte. Und es gibt immer noch etwas zu überarbeiten und zu verbessern.&lt;/p&gt;&#10;&lt;h2 id="übersicht"&gt;Übersicht&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Unit-Test-Suite mit über 250 Testfällen&lt;/li&gt;&#10;&lt;li&gt;Serielle Debug-Ausgabe konfigurierbar&lt;/li&gt;&#10;&lt;li&gt;Lose gekoppelte, objektorientierte Architektur in &lt;code&gt;C++&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Einzelne Module mit klaren Aufgaben, skalierbar und leicht zugänglich für zukünftige Funktionen&lt;/li&gt;&#10;&lt;li&gt;benutzerdefinierte Hardware-Abstraktionsschicht; der größte Teil des Codes sollte ohne Änderungen auf andere MCUs portierbar sein&lt;/li&gt;&#10;&lt;li&gt;eigenes Dependency Injection Framework (Loader Klasse)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="funktionen"&gt;Funktionen&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Konfigurierbare Benutzereingabe (Tasten oder Encoder)&lt;/li&gt;&#10;&lt;li&gt;Auto-Poweroff, wenn für eine konfigurierbare Zeit keine Taste gedrückt wird&lt;/li&gt;&#10;&lt;li&gt;Einschalten durch Drücken der &amp;ldquo;Play&amp;rdquo;-Taste&lt;/li&gt;&#10;&lt;li&gt;Autoplay-Funktion&lt;/li&gt;&#10;&lt;li&gt;Status-Led-Funktion&lt;/li&gt;&#10;&lt;li&gt;Sperren/Entsperren der Benutzereingabe&lt;/li&gt;&#10;&lt;li&gt;Mehrere Wiedergabemodi [Album, Zufall, Speichern des Titelfortschritts, Nur ein Titel] pro Nfc-Tag verfügbar&lt;/li&gt;&#10;&lt;li&gt;Sprachmenüs zum Verknüpfen oder Löschen von Nfc-Tags&lt;/li&gt;&#10;&lt;li&gt;Optimiert für Batterieanwendungen (z.B. Powerbank) unter Verwendung von Ruhezuständen&lt;/li&gt;&#10;&lt;li&gt;Niedriger Stromverbrauch &lt;code&gt;@5V&lt;/code&gt;: &lt;code&gt;~40mA&lt;/code&gt; im Leerlauf, &lt;code&gt;~75mA&lt;/code&gt; bei mittlerer Lautstärke&lt;/li&gt;&#10;&lt;li&gt;Konfigurationsdatei für Startlautstärke, Einschlaf-Timer etc.&lt;/li&gt;&#10;&lt;li&gt;Automatische Wiederherstellung bei Eingabeaufforderung, die niemand bedient&lt;/li&gt;&#10;&lt;li&gt;Sprachansagen für die häufigsten Fehlermeldungen&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="nicht-enthaltene-funktionen"&gt;Nicht enthaltene Funktionen&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Keine Erkennung des Ladezustands der Powerbank&lt;/li&gt;&#10;&lt;li&gt;Kein Konfigurationsmenü (Startlautstärke, Dauer des Einschlaf-Timers, Standby-Dauer usw.)&lt;/li&gt;&#10;&lt;li&gt;&amp;hellip;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="dokumentation"&gt;Dokumentation&lt;/h2&gt;&#10;&lt;p&gt;Das Projekt ist für das Arduino Framework erstellt - getestet auf einem Arduino nano Board - unter Verwendung der &lt;a href="https://platformio.org" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PlatformIO IDE&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&#10;Die folgenden Abschnitte zeigen das Design.&lt;/p&gt;&#10;&lt;h3 id="projekt-modulübersicht"&gt;Projekt Modulübersicht&lt;/h3&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Ordnername in &lt;code&gt;/lib&lt;/code&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Zweck&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Arduino&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;minimalistische hardwarenahe Implementierungen, nicht Unit-testbar&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Arduino_HardwareAbstraction&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Hardware-Abstraktion, um Portabilität und Testen zu ermöglichen&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Config&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Systemkonfigurationsparameter&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Folder&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Geschäftslogik für Wiedergabeliste und Wiedergabemodus&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Loader&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Dependency Injection Framework&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;MessageHandler&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Systemnachrichten und Debug-Framework&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Mp3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Mp3-Steuerung (Status, Ordner, Sprachausgabe, Anzeigen)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Nfc&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Tag-Steuerung (Status, Lesen, Schreiben, Löschen)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;PowerManager&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Steuerung von Status-LED und Ruhezustand abhängig vom Systemstatus&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Tonuino&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Haupt-Task-Scheduler&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;UserInput&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Verarbeitung von Tasten- oder Encoder-Eingaben&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Utilities&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Timer, Led-Steuerung, Pin-Steuerung&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;VoiceMenu&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Geschäftslogik für das Menü Link / Löschen / Konfiguration&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="verwendete-externe-bibliotheken"&gt;Verwendete externe Bibliotheken&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://platformio.org/lib/show/131/TimerOne" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Arduino&amp;rsquo;s TimerOne&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://platformio.org/lib/show/7212/SoftwareSerial" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Arduino&amp;rsquo;s SoftwareSerial&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://platformio.org/lib/show/11808/ClickEncoder" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Schallbert&amp;rsquo;s ClickEncoder&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://platformio.org/lib/show/1561/DFPlayer%20Mini%20Mp3%20by%20Makuna" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Makuna&amp;rsquo;s DFMini Mp3&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://platformio.org/lib/show/63/MFRC522" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Miguel Balboa&amp;rsquo;s MFRC522&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Automatische Installation durch Platformios &amp;ldquo;Library Dependency Finder&amp;rdquo; beim erstem Kompiliervorgang&lt;/p&gt;&#10;&lt;h3 id="klassendiagramme"&gt;Klassendiagramme&lt;/h3&gt;&#10;&lt;p&gt;Klassendiagramme wären zu viel nicht-automatisierte Arbeit gewesen. Stattdessen habe ich das hier vorbereitet:&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/Schallbert/Tonuino_alternativeFirmware/master/docs/ProjectModulesOverview.png" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Software-Modulübersicht&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Sie sollte ein genaues Verständnis dafür vermitteln, wie die Softwaremodule zusammenwirken und welche APIs die Module anbieten.&lt;/p&gt;&#10;&lt;h2 id="los-gehts"&gt;Los geht&amp;rsquo;s!&lt;/h2&gt;&#10;&lt;p&gt;Zuerst muss mein &lt;a href="https://github.com/Schallbert/Tonuino_alternativeFirmware" target="_blank" rel="noopener noreferrer" class="external-link"&gt;repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, geklont werden - entweder per &lt;code&gt;git clone&lt;/code&gt;, Download als &lt;code&gt;zip&lt;/code&gt;-Datei oder per &lt;code&gt;https&lt;/code&gt;. Das &lt;code&gt;README.md&lt;/code&gt; Dokument bietet einen leichten Einstieg. Ich empfehle die Verwendung von PlatformIO für dieses Projekt.&#10;In den folgenden Abschnitten sind die Teilfunktionen des Programms erläutert, und wie deren korrekte Funktion sichergestellt wird.&lt;/p&gt;&#10;&lt;h2 id="testen"&gt;Testen&lt;/h2&gt;&#10;&lt;h3 id="unit-tests"&gt;Unit-Tests&lt;/h3&gt;&#10;&lt;p&gt;Unit-Tests wurden mit dem &lt;code&gt;gtest&lt;/code&gt; C++ Unit-Test-Framework geschrieben. Sie befinden sich im Ordner &lt;code&gt;test/desktop&lt;/code&gt;. Zu beachten ist, dass &lt;em&gt;googletest&lt;/em&gt; die Installation von &lt;code&gt;gcc&lt;/code&gt; mit einigen Bibliotheken voraussetzt. Die Anleitung dazu findet man &lt;a href="https://blog.schallbert.de/projects/platformio_gtestgmock/"&gt;in einem meiner anderen Projekte&lt;/a&gt;.&#10;Sobald diese Voraussetzungen erfüllt sind und ein einfacher &lt;code&gt;ASSERT_TRUE(false);&lt;/code&gt;-Test mit der gtest-Umgebung erwartungsgemäß fehlschlägt, können die Unit-Tests des Projekts erstellt und mit &lt;code&gt;pio test -e desktop -f desktop&lt;/code&gt;, in der PlatformIO CLI (Terminal) ausgeführt werden.&lt;/p&gt;&#10;&lt;h3 id="akzeptanztests"&gt;Akzeptanztests&lt;/h3&gt;&#10;&lt;p&gt;Obwohl diese Tests automatisiert werden könnten, ist es viel einfacher, diese Tests von Hand durchzuführen, nachdem der Mikrocontroller programmiert und alle elektronischen Komponenten zusammengesteckt wurden.&#10;Jede Zeile in den folgenden Tabellen ist ein eigenständiger Testfall. Die Testsuiten (&amp;ldquo;Überschrift Name&amp;rdquo;) haben eine &lt;strong&gt;System Pre:&lt;/strong&gt; Eigenschaft, die vor der Ausführung der einzelnen Testfälle wiederhergestellt werden muss. Wenn die Erwartungsklausel erfüllt ist, ist der Test &lt;strong&gt;BESTANDEN&lt;/strong&gt;. Ich habe die Tests auf Englisch belassen, weil sie recht eng mit dem Code korrespondieren.&lt;/p&gt;&#10;&lt;h4 id="switching-on"&gt;Switching ON&lt;/h4&gt;&#10;&lt;p&gt;System Pre: System is OFF&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Action&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expectation&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;LED flashes slowly?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Play/pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Welcome prompt plays?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h4 id="switching-off"&gt;Switching OFF&lt;/h4&gt;&#10;&lt;p&gt;System Pre: System is ON&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Action&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expectation&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Playback on pause, no button input&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;System switches off after a time?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;System shutdown&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Farewell prompt plays?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;System shutdown&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;LED switched off?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h4 id="play-help-prompt"&gt;Play Help Prompt&lt;/h4&gt;&#10;&lt;p&gt;System Pre: System is ON&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Action&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expectation&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Long press Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Help prompt plays?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Help prompt playing&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Can be interrupted with any button press?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h4 id="behavior-without-tag"&gt;Behavior without Tag&lt;/h4&gt;&#10;&lt;p&gt;System Pre: System is ON, no Tag present&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Action&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expectation&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;prompts &amp;ldquo;couldn&amp;rsquo;t find track&amp;rdquo; error?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Next button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;prompts &amp;ldquo;couldn&amp;rsquo;t find track&amp;rdquo; error?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Prev button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;prompts &amp;ldquo;couldn&amp;rsquo;t find track&amp;rdquo; error?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Delete Menu prompt played?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h4 id="behavior-with-tag"&gt;Behavior with Tag&lt;/h4&gt;&#10;&lt;p&gt;System Pre: System is ON, linked Tag available&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Precondition&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Action&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expectation&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;no Tag placed&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;place known Tag&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;starts Playback of correct Folder?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Next button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;plays next track?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Next button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;next track in accordance with selected playMode of Folder?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;long press Next button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;increases volume?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Prev button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;plays previous track?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Prev button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;previous track in accordance with selected playMode of Folder?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;long press Prev button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;decreases volume?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;pauses track?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;paused playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;resumes track?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Lock prompt played?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Playback resumes after Lock prompt played?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;locks button input?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;locked button input&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Unlock prompt played?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;locked button input&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Playback resumes after Unlock prompt played?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;locked button input&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;unlocks button input?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;paused playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Playback resumes after Lock prompt played?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h4 id="behavior-with-unlinked-tag"&gt;Behavior with unlinked Tag&lt;/h4&gt;&#10;&lt;p&gt;System Pre: System is ON&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Action&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expectation&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;place unlinked Tag&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;plays LinkMenu Prompt?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;navigate through LinkMenu&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;plays Configuration Success Prompt?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;place Tag again&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;plays linked folder?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h2 id="hardware"&gt;Hardware&lt;/h2&gt;&#10;&lt;h3 id="materialliste"&gt;Materialliste&lt;/h3&gt;&#10;&lt;p&gt;Ich habe versucht mit möglichst wenigen Einzelteilen auszukommen. Für ein Projekt dieser Größe ist die Liste denke ich recht kurz geblieben.&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Menge&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Artikel&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Zweck&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Arduino (z.B. Nano)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Gehirn&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Dfplayer Mini Mp3 Player&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Mund&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Micro SD Karte&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Speicher&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;MFRC522 Nfc-Leser&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Psi-Sense&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Bistabiles Relais 5V, z.B. HFE20&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kaffee - kein Schlaf&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Dioden z.B. 1n4007&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Venenklappe&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;PNP-Transistor z. B. BC327&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kaffeemaschinenschalter&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Widerstände 1k&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Blutstromaufbereiter&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Widerstand 220&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Blinkstärke dimmen&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;LED, Farbe nach Wahl&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;blinken&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Powerbank/ 5V Versorgung&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Lebensmittel&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Drucktasten, z.B. Cherry MX Tasten&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Drucksensoren&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Lautsprecher, z.B. 5W@4Ohms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Stimmbänder&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;USB-A-Stecker&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Strohhalm&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Gehäuse&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Körper&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="zusätzliche-bauteile-und-tipps"&gt;Zusätzliche Bauteile und Tipps&lt;/h3&gt;&#10;&lt;p&gt;Überbrückungsdrähte, Platine, Buchsen und Verbrauchsmaterial nach eigenem Ermessen&#10;Wie beim &amp;ldquo;Original&amp;rdquo;-Projekt; alternativ einen Drehgeber anstelle der Drucktasten kaufen und das Projekt so konfigurieren, dass er statt der Taster ausgelesen wird.&#10;Da das System für den Batteriebetrieb optimiert ist, nehmen Sie ein bi-stabiles Relais oder einen JFET-Transistor (mit niedriger Gate-Spannung und niedrigem Source-Drain-Spannungsabfall) für die KeepAlive-Funktionalität, die den zusätzlichen Strom durch die ständig eingeschalteten Relaisspulen vermeidet. Eine billige Powerbank reicht für viele Stunden Betrieb. Alternativ können z.B. 3xAA-Batterien verwendet werden - allerdings macht der Dfmini-Player ordentlich Lärm, wenn die Spannung zu sehr einbricht. Er kann sogar bei wiederholten Neustarts durch Stromstöße zerstört werden, also Vorsicht bei nachlassender Akkuladung.&lt;/p&gt;&#10;&lt;h3 id="schaltplan"&gt;Schaltplan&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://raw.githubusercontent.com/Schallbert/Tonuino_alternativeFirmware/master/docs/CircuitDiagram.jpg" alt="Stromlaufplan"&gt;&lt;/figure&gt;&#10;</description></item><item><title>Umzug von Github Pages</title><link>https://blog.schallbert.de/projects/move-blog-to-own-server/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/move-blog-to-own-server/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/movetoownserver/giteawebdashboard-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Gitea Dashboard, served on my machine, proxied by caddy"&#10; title="Umzug von Github Pages" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeitsgrad: Mittel 3/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: 5-50€/Monat&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~10h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Ich habe mich entschieden, &lt;a href="https://blog.schallbert.de/projects/thissite/"&gt;meine Website&lt;/a&gt; von &lt;a href="https://pages.github.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github-Pages&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; auf meinen eigenen Server umzuziehen. Dies geschieht aus zwei Gründen: Zum einen möchte ich den Server in Deutschland stehen haben (Datenschutz) und zum anderen bietet mir Github Pages keine einfach zu handhabende Möglichkeit, meine Site zweisprachig anzubieten.&lt;/p&gt;&#10;&lt;p&gt;Zusätzlich kann ich ebenfalls ein paar lang gehegte Wünsche realisieren: Ein eigener mini-Dateiserver und eine Webseite für meinen kleinen Nebenerwerb werden jetzt möglich. Aufsetzen werde ich sie allerdings in einem späteren Schritt.&lt;/p&gt;&#10;&lt;h2 id="überblick"&gt;Überblick&lt;/h2&gt;&#10;&lt;p&gt;In diesem Projekt werde ich einen virtuellen Server anmieten und dort per &lt;code&gt;SSH&lt;/code&gt; das Container-Managementsystem &lt;a href="https://www.docker.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; installieren.&lt;/p&gt;&#10;&lt;p&gt;In einem Container soll eine Instanz von &lt;a href="https://docs.gitea.com/next/installation/install-with-docker#startup" target="_blank" rel="noopener noreferrer" class="external-link"&gt;gitea&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; laufen, welche das Versionsmanagement für mich übernehmen und über sogenannte Actions eine CI/CD-Pipeline&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; bereitstellen wird.&lt;/p&gt;&#10;&lt;p&gt;Durch die Automation mit &lt;a href="https://docs.gitea.com/next/usage/actions/overview" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea Actions&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; möchte ich dann in der Lage sein, die von &lt;code&gt;Jekyll&lt;/code&gt; per &lt;a href="https://docs.gitea.com/next/usage/actions/quickstart#use-actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Workflow-Datei&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gebaute Site auf einem &lt;code&gt;caddy&lt;/code&gt;-Server zu veröffentlichen, also in &amp;ldquo;Produktion&amp;rdquo; zu geben.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://caddyserver.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;caddy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; stellt hier nicht nur den Server, sondern auch einen &lt;a href="https://en.wikipedia.org/wiki/Reverse_proxy" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Reverse Proxy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Verfügung, welcher mir Portmapping und die Adresszuordnung abnehmen kann. Ganz nebenbei kümmert sich &lt;code&gt;caddy&lt;/code&gt; noch vollautomatisiert um die Zertifikatsverwaltung für &lt;code&gt;https&lt;/code&gt; über &lt;a href="https://letsencrypt.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Let&amp;rsquo;s Encrypt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Da auch caddy in Docker laufen wird, müssen virtuelle Netzwerke angelegt und korrekt konfiguriert werden, sodass die Container miteinander &amp;ldquo;reden&amp;rdquo; können.&lt;/p&gt;&#10;&lt;p&gt;Schreiten wir zur Tat - aber immer Schritt für Schritt.&lt;/p&gt;&#10;&lt;h2 id="konfiguration-meines-servers"&gt;Konfiguration meines Servers&lt;/h2&gt;&#10;&lt;p&gt;Die folgenden Abschnitte beschreiben Schritt für Schritt, wie ich vorgegangen bin, meinen Cloud-Server einzurichten und für das Veröffentlichen dieses Blogs vorzubereiten.&lt;/p&gt;&#10;&lt;h3 id="ssh-auf-dem-server-einrichten"&gt;SSH auf dem Server einrichten&lt;/h3&gt;&#10;&lt;p&gt;Direkt nachdem ich den sogenannten &amp;ldquo;Cloud-Server&amp;rdquo; angemietet hatte, habe ich den Zugriff per SSH konfiguriert und damit den Passwort-Login deaktiviert.&#10;Dafür prüfe ich zuerst, ob mein &lt;code&gt;ssh-agent&lt;/code&gt; bereits läuft bzw. starte ihn direkt mit dem Befehl &lt;code&gt;eval ssh-agent&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Sodann erstelle ich mir ein neues Schlüsselpaar per Befehl&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;ssh-keygen -t ed25519 -C &amp;quot;your_email@example.com&amp;quot; -f &amp;quot;~/.ssh/mein-cloud-server&amp;quot;&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Dieses Beispiel erzeugt Schlüsseldateien mit dem Verfahren (&lt;code&gt;-t&lt;/code&gt;) &lt;code&gt;ed25519&lt;/code&gt;, welche per Kommentar (&lt;code&gt;-C&lt;/code&gt;) eine Mailadresse enthalten und unter dem Dateinamen (&lt;code&gt;-f&lt;/code&gt;) &lt;code&gt;mein-cloud-server&lt;/code&gt; gespeichert werden.&lt;/p&gt;&#10;&lt;p&gt;Den öffentlichen Schlüssel &lt;code&gt;mein-cloud-server.pub&lt;/code&gt; habe ich dann im Webportal meines Servers unter &amp;ldquo;Security&amp;rdquo; eingegeben und den dann erhaltenen Fingerprint verifiziert mittels&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;ssh-keygen -lf &amp;lt;fingerprint_from_server&amp;gt; &amp;quot;~/.ssh/mein-cloud-server&amp;quot;&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Anschließend habe ich die &lt;code&gt;config&lt;/code&gt;-Datei im &lt;code&gt;.ssh&lt;/code&gt;-Verzeichnis so angepasst, dass ich nur noch &lt;code&gt;ssh server&lt;/code&gt; statt der IP-Adresse eingeben muss, um mich drauf zu verbinden - Faulheit siegt 😀&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#.ssh/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Host server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;User root&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Hostname ip_address_of_server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;PreferredAuthentications publickey&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;IdentityFile ~/.ssh/mein-cloud-server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Gebe ich nun &lt;code&gt;ssh server&lt;/code&gt; in die Konsole ein, so erscheint beim ersten Mal die Meldung, dass der Verbindungsaufbau zu einem unbekannten Partner mit Fingerprint soundso stattfinden wird und bittet um explizite Zustimmung. Nun kann der Fingerprint mit dem des Servers (auf der Webseite des Anbieters) abgeglichen und somit sichergestellt werden, dass man auch wirklich zu dem angestrebten, eigenen Server verbindet und nicht zu einem Dritten, der die Anfragen lediglich transparent weiterleitet. Stimmt man mit &lt;code&gt;yes&lt;/code&gt; zu, so wird der Fingerprint der &lt;code&gt;known_hosts&lt;/code&gt;-Datei hinzugefügt und das Zielsystem in Zukunft als bekannt angesehen.&lt;/p&gt;&#10;&lt;h2 id="docker-installieren-"&gt;Docker installieren 🐳&lt;/h2&gt;&#10;&lt;p&gt;Auf meinem Server läuft ein Standard-Ubuntu, für die die von Docker bereitgestellte &lt;a href="https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Installation des Docker-Repository per &lt;code&gt;apt&lt;/code&gt; reibungslos funktioniert. Einfach per SSH drauf einwählen und die entsprechenden Befehle ausführen - fertig. Dabei wird der von mir benutzte Dienst &lt;code&gt;docker-compose&lt;/code&gt; gleich mit installiert, welcher Konfigurationsdateien für die Zielcontainer interpretieren und die Container mit den entsprechenden Parametern gleich richtig hoch- und herunterfahren kann.&lt;/p&gt;&#10;&lt;h2 id="caddy"&gt;caddy installieren 🛒&lt;/h2&gt;&#10;&lt;p&gt;Zuerst lege ich den Ordner per &lt;code&gt;mkdir caddy2&lt;/code&gt; im Stammverzeichnis meines Servers an - soviel Zeit muss sein. Um caddy im Docker laufen lassen zu können, habe ich in diesem Ordner die folgende &lt;code&gt;docker-compose.yml&lt;/code&gt; erstellt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#caddy2/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;3&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;services&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;caddy&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;caddy:latest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;caddy&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ports&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;80:80&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;443:443&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;ACME_AGREE=true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;restart&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;unless-stopped&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./Caddyfile:/etc/caddy/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./caddy_data:/data&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./caddy_config:/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./www:/www&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;networks&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;caddy-proxy&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;networks&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;caddy-proxy&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;external&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diese Datei sagt Docker, dass es für den Service das aktuellste Image von &lt;code&gt;caddy&lt;/code&gt; verwenden und dem Container den Namen &lt;code&gt;caddy&lt;/code&gt; geben soll. Des Weiteren sollen die Ports &lt;code&gt;80&lt;/code&gt; (http) sowie &lt;code&gt;443&lt;/code&gt; (https) verfügbar gemacht und die Zertifikatsverwaltung per &lt;a href="https://www.howtogeek.com/devops/how-to-request-a-letsencrypt-certificate-using-acme/#installing-acme-sh" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;ACME_AGREE&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zugelassen werden. Der Container bindet die von Caddy benötigten Dateien (Caddyfile, data, config) ein und kann ebenso auf die von mir unter &lt;code&gt;/www&lt;/code&gt; abgelegten Dateien zum späteren Veröffentlichen meiner Website zugreifen. Der Container befindet sich im Netzwerk &lt;code&gt;caddy-proxy&lt;/code&gt; und durch &lt;code&gt;external: true&lt;/code&gt; weiß der Container, dass dieses Netzwerk unabhängig von diesem Container bereits besteht.&lt;/p&gt;&#10;&lt;p&gt;Damit wir den caddy-Container korrekt aufsetzen können, müssen wir also das Docker-Netzwerk vorher per &lt;code&gt;docker network create caddy-proxy&lt;/code&gt; erstellen. Mit &lt;code&gt;docker network ls&lt;/code&gt; kann man nun prüfen, ob das Netzwerk korrekt angelegt wurde.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;NETWORK ID NAME DRIVER SCOPE&#10;&amp;lt;redacted&amp;gt; bridge bridge local&#10;&amp;lt;redacted&amp;gt; caddy2_default bridge local&#10;&amp;lt;redacted&amp;gt; caddy-proxy bridge local&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Sieht gut aus!&lt;/p&gt;&#10;&lt;p&gt;Anschließend erstelle ich eine &lt;code&gt;Caddyfile&lt;/code&gt; zur Konfiguration von Caddy:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# caddy2/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;blog.schallbert.de {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;root * /www/blog&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;encode gzip&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;file_server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dieser Eintrag sagt caddy, dass der Inhalt des Ordners &lt;code&gt;/www/blog&lt;/code&gt; als Dateiserver, der die &lt;code&gt;gzip&lt;/code&gt;-Komprimierung unterstützt, unter &lt;a href="https://blog.schallbert.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;blog.schallbert.de&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; verfügbar machen soll.&lt;/p&gt;&#10;&lt;h3 id="test-website-erstellen-"&gt;Test-Website erstellen 🧪&lt;/h3&gt;&#10;&lt;p&gt;Ach ja, was auch noch wichtig ist, bevor wir den Container starten: Der DNS&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; sollte die Adresse auch auflösen können. Dafür logge ich mich in meinem DNS-Manager ein und ordne der Subdomain die feste IP-Adresse meines Servers zu:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;TYPE NAME VALUE TTL&#10;A blog.schallbert.de &amp;lt;redacted&amp;gt; 86400&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Schlussendlich soll der Server ja auch etwas anzeigen können. Eine Testseite genügt ja schon. Also ganz fix:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir ~/caddy2/www/blog&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd ~/caddy2/www/blog&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nano index.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und dort dann ein unverkennbares Hello World eintragen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- blog/index.html --&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;h1&lt;/span&gt;&amp;gt;ALL YOUR BASE ARE BELONG TO US&amp;lt;/&lt;span style="color:#f92672"&gt;h1&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Speichern, fertig.&lt;/p&gt;&#10;&lt;p&gt;Das Stammverzeichnis von caddy sieht nun so aus:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;*caddy_config* *caddy_data* Caddyfile docker-compose.yml *www*&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Jetzt endlich kann der Container erstellt und caddy darin hochgefahren werden:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose up -d&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die Option &lt;code&gt;-d&lt;/code&gt; bedeutet &amp;ldquo;detach&amp;rdquo;, also wird der Container hochgefahren und von der Konsole abgekoppelt, sodass sie nicht blockiert wird.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;[+] Running 1/1&#10; ✔ Container caddy Started&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Super, das sieht auch gut aus. Jetzt schnell mal auf die Seite navigieren:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/movetoownserver/address.jpg" alt="Image: caddy is displaying the test page correctly"&gt;&lt;/figure&gt;&#10;&lt;h2 id="gitea-installieren-"&gt;Gitea installieren 🍵&lt;/h2&gt;&#10;&lt;p&gt;Wie oben kurz angerissen, benötige ich Gitea für zwei Zwecke: Erstens möchte ich für meine Dateien wie von Github gewohnt ein Versionsmanagement haben und zweitens möchte ich meine Seite mit Hilfe von Gitea über Jekyll bauen und dann auf den Server schieben.&lt;/p&gt;&#10;&lt;p&gt;Wie schon bei caddy, beginnt alles in einem entsprechenden Ordner:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd ~&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir gitea&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd gitea&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nano docker-compose.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die &lt;code&gt;docker-compose.yml&lt;/code&gt; für Gitea habe ich mir aus verschiedenen &lt;a href="https://docs.gitea.com/installation/install-with-docker" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Beispielen zusammengebastelt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Hier ist sie:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;3&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;networks&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;gitea&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;external&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;caddy-proxy&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;external&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;caddy-proxy&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;services&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;gitea&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/gitea:latest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;restart&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;always&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;USER_UID=1000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;USER_GID=1000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;SSH_DOMAIN=git.schallbert.de&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_HOSTNAME=git.schallbert.de&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;networks&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;caddy-proxy&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./gitea:/data&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./log:/app/gitea/log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/etc/timezone:/etc/timezone:ro&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ports&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;127.0.0.1:3000:3000&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;222:22&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runner&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/act_runner:nightly&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;CONFIG_FILE=/config.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_INSTANCE_URL=https://git.schallbert.de&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_RUNNER_NAME=ichlaufe&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_RUNNER_REGISTRATION_TOKEN=&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./runner/config.yml:/config.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./runner/data:/data&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die Bedeutung ist in etwa wie folgt: &lt;code&gt;gitea&lt;/code&gt; ist Teil der Docker-Netzwerke &lt;code&gt;gitea&lt;/code&gt; sowie &lt;code&gt;caddy-proxy&lt;/code&gt;. Der Container, in dem der Service läuft, heißt &lt;code&gt;gitea&lt;/code&gt; und wird bei Absturz stets automatisch neu gestartet. Die User-ID wird mit &lt;code&gt;1000&lt;/code&gt; in einem Wertebereich gesetzt, welcher weder für Administratoren noch andere besondere Benutzer reserviert ist. Per SSH und http ist gitea unter &lt;code&gt;git.schallbert.de&lt;/code&gt; erreichbar, und zwar auf Ports &lt;code&gt;22&lt;/code&gt; bzw. &lt;code&gt;3000&lt;/code&gt; - dort aber nur per localhost, also nicht &amp;ldquo;von außen&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;Gitea hat Zugriff auf Zeitzone und Lokalzeit des Servers, außerdem legt es Logs unter &lt;code&gt;/log&lt;/code&gt; und Dateien unter &lt;code&gt;/data&lt;/code&gt; ab. Gitea verfügt über einen &lt;code&gt;action runner&lt;/code&gt;, der auf der gitea-Instanz läuft, &lt;code&gt;ichlaufe&lt;/code&gt; heißt und dessen Konfiguration in der &lt;code&gt;config.yml&lt;/code&gt; zu finden ist. Der Runner kann im Docker über dessen &lt;a href="https://stackoverflow.com/questions/35110146/what-is-the-purpose-of-the-file-docker-sock" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Standard-Socket&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Aktionen ausführen, z.B. &lt;code&gt;ubuntu-latest&lt;/code&gt; installieren und darauf &lt;code&gt;Jekyll&lt;/code&gt; zum Laufen bringen. Die Basiskonfiguration des Runners habe ich &lt;a href="https://gitea.com/gitea/act_runner/src/branch/main/examples/docker-compose" target="_blank" rel="noopener noreferrer" class="external-link"&gt;dem offiziellen Repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; entnommen.&lt;/p&gt;&#10;&lt;p&gt;Gitea akzeptiert nur Runner, deren Token unter &lt;code&gt;GITEA_RUNNER_REGISTRATION_TOKEN&lt;/code&gt; registriert sind - Dies holen wir zu einem späteren Zeitpunkt nach.&lt;/p&gt;&#10;&lt;p&gt;Nun starten wir Gitea mal, sodass wir die erforderlichen Ordner zur Konfiguration erstellt bekommen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose up&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So sieht die Konsolenausgabe bei mir aus:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;[+] Running 4/2&#10; ✔ Network gitea_default Created 0.0s &#10; ✔ Network gitea_gitea Created 0.1s &#10; ✔ Container gitea Created 0.1s &#10; ✔ Container gitea-runner-1 Created 0.0s &#10;Attaching to gitea, gitea-runner-1&#10;gitea | Server listening on :: port 22.&#10;gitea | Server listening on 0.0.0.0 port 22.&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:242:runWeb() [I] Starting Gitea on PID: 15&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:111:showWebStartupMessage() [I] Gitea version: 1.21.0 built with GNU Make 4.4.1, go1.21.4 : bindata, timetzdata, sqlite, sqlite_unlock_notify&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:112:showWebStartupMessage() [I] * RunMode: prod&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:113:showWebStartupMessage() [I] * AppPath: /usr/local/bin/gitea&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:114:showWebStartupMessage() [I] * WorkPath: /data/gitea&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:115:showWebStartupMessage() [I] * CustomPath: /data/gitea&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:116:showWebStartupMessage() [I] * ConfigFile: /data/gitea/conf/app.ini&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:117:showWebStartupMessage() [I] Prepare to run web server&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="web-access"&gt;Webzugriff einrichten&lt;/h3&gt;&#10;&lt;p&gt;Nun gehe ich erneut in meinen DNS-Manager und setze die Adresse für den Webzugriff:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;TYPE NAME VALUE TTL&#10;A git.schallbert.de &amp;lt;redacted&amp;gt; 86400&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Außerdem muss die &lt;code&gt;Caddyfile&lt;/code&gt; angepasst werden, damit Anfragen aus dem Web auch richtig weitergeleitet werden können. Also zurück in den &lt;code&gt;caddy2&lt;/code&gt; Ordner und mit &lt;code&gt;nano Caddyfile&lt;/code&gt; folgenden Abschnitt hinzufügen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# caddy2/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;git.schallbert.de {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;reverse_proxy * http://gitea:3000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nun werden Anfragen nach &lt;code&gt;git.schallbert.de&lt;/code&gt; auf den im Docker geöffneten Port &lt;code&gt;3000&lt;/code&gt; des Gitea-Containers weitergeleitet.&lt;/p&gt;&#10;&lt;h3 id="gitea-per-web-oberfläche-konfigurieren"&gt;Gitea per Web-Oberfläche konfigurieren&lt;/h3&gt;&#10;&lt;p&gt;Navigiere ich im Browser jetzt auf die entsprechende Adresse, so bekomme ich ein Login-Fenster angezeigt - genau wie in &lt;a href="https://gitea.com/user/login" target="_blank" rel="noopener noreferrer" class="external-link"&gt;dieser Demo&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Nun denke ich mir ein schönes Passwort aus, klicke auf &lt;code&gt;Register&lt;/code&gt; und bekomme hernach die Möglichkeit, Gitea per Weboberfläche zu konfigurieren.&#10;Dies tue ich aber nur rudimentär, denn ich möchte am Ende ja meine eigene Konfiguration verwenden.&lt;/p&gt;&#10;&lt;p&gt;Anschließend erstelle ich wie im Kapitel &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/#ssh-auf-dem-server-einrichten"&gt;SSH einrichten&lt;/a&gt; oben beschrieben ein weiteres SSH-Schlüsselpaar per &lt;code&gt;ssh-keygen&lt;/code&gt; und passendem Dateinamen &lt;code&gt;-f ~/.ssh/gitea&lt;/code&gt;. Diesen lade ich dann auf Gitea unter &lt;code&gt;Settings-&amp;gt;SSH/GPG Keys&lt;/code&gt; hoch und verifiziere ihn.&lt;/p&gt;&#10;&lt;p&gt;Nun kann ich auf meinem Rechner erneut in der &lt;code&gt;config&lt;/code&gt;-Datei des &lt;code&gt;.ssh&lt;/code&gt;-Verzeichnisses den Abschnitt zum Login auf Gitea erstellen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# .ssh/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Host gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;User schallbert&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Hostname git.schallbert.de&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;PreferredAuthentications publickey&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Identityfile ~/.ssh/schallbert_gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Jetzt schnell prüfen, ob auch alles funktioniert:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh gitea&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host &lt;span style="color:#e6db74"&gt;&amp;#39;git.schallbert.de&amp;#39;&lt;/span&gt; is known and matches the ED25519 host key.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;debug1: Found key in /home/schallbert/.ssh/known_hosts&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Perfekt.&lt;/p&gt;&#10;&lt;h3 id="gitea-konfiguration-per-appini-fertigstellen"&gt;Gitea-Konfiguration per &lt;code&gt;app.ini&lt;/code&gt; fertigstellen&lt;/h3&gt;&#10;&lt;p&gt;Da die Konfiguration aber noch nicht abgeschlossen ist, fahre ich den Container nun wieder herunter:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose down&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;und navigiere nach &lt;code&gt;/gitea/gitea/conf&lt;/code&gt;. Dort öffne ich die &lt;code&gt;app.ini&lt;/code&gt; und modifiziere sie wie folgt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# conf/app.ini&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;APP_NAME&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Gitea: Git with a cup of tea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;RUN_MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;prod&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;RUN_USER&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;git&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;WORK_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[repository]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ROOT&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/git/repositories&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DEFAULT_BRANCH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;main&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[repository.local]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;LOCAL_COPY_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/tmp/local-repo&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[repository.upload]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;TEMP_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/uploads&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[server]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;APP_DATA_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DOMAIN&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;git.schallbert.de&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;SSH_DOMAIN&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;git.schallbert.de&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;HTTP_PORT&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;3000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ROOT_URL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;https://git.schallbert.de/&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DISABLE_SSH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;SSH_PORT&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;222&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;SSH_LISTEN_PORT&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;22&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;LFS_START_SERVER&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;LFS_JWT_SECRET&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;lt;redacted&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;OFFLINE_MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[database]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/gitea.db&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DB_TYPE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;sqlite3&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;HOST&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;localhost:3306&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;NAME&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;USER&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;root&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PASSWD&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;LOG_SQL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;SCHEMA&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;SSL_MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;disable&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[indexer]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ISSUE_INDEXER_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/indexers/issues.bleve&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[session]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PROVIDER_CONFIG&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/sessions&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PROVIDER&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[picture]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;AVATAR_UPLOAD_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/avatars&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REPOSITORY_AVATAR_UPLOAD_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/repo-avatars&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[attachment]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/attachments&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[log]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;LEVEL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;info&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ROOT_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[security]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;INSTALL_LOCK&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;SECRET_KEY&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REVERSE_PROXY_LIMIT&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REVERSE_PROXY_TRUSTED_PROXIES&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;*&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;INTERNAL_TOKEN&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;lt;redacted&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PASSWORD_HASH_ALGO&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;pbkdf2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[service]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DISABLE_REGISTRATION&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REQUIRE_SIGNIN_VIEW&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REGISTER_EMAIL_CONFIRM&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLE_NOTIFY_MAIL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ALLOW_ONLY_EXTERNAL_REGISTRATION&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLE_CAPTCHA&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DEFAULT_KEEP_EMAIL_PRIVATE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DEFAULT_ALLOW_CREATE_ORGANIZATION&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DEFAULT_ENABLE_TIMETRACKING&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;NO_REPLY_ADDRESS&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;noreply.localhost&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[lfs]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/git/lfs&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[mailer]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLED&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[openid]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLE_OPENID_SIGNIN&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLE_OPENID_SIGNUP&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[cron.update_checker]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLED&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[repository.pull-request]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DEFAULT_MERGE_STYLE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;merge&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[repository.signing]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DEFAULT_TRUST_MODEL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;committer&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[oauth2]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;JWT_SECRET&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;lt;redacted&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[actions]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLED&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hier ist wichtig zu sicherzustellen, dass &lt;code&gt;ROOT_URL&lt;/code&gt; und die Portkonfiguration mit jener der &lt;code&gt;docker-compose.yml&lt;/code&gt; übereinstimmt. Die Datenbank (sqlite3) wird konfiguriert und gitea zugänglich gemacht. Außerdem deaktiviere ich aus Sicherheitsgründen die Registrierungsmöglichkeit neuer Benutzer &lt;code&gt;DISABLE_REGISTRATION = true&lt;/code&gt; sowie jene per Openid &lt;code&gt;ENABLE_OPENID_SIGNIN = false ENABLE_OPENID_SIGNUP = false&lt;/code&gt; und schalte Gitea Actions per &lt;code&gt;ENABLED = true&lt;/code&gt; ein.&lt;/p&gt;&#10;&lt;p&gt;Nun kann der Container neu gestartet werden:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;+&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Running 4/4&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ✔ Network gitea_default Created 0.1s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ✔ Network gitea_gitea Created 0.0s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ✔ Container gitea Started 0.0s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; x Container gitea-runner-1 Error 0.1s &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Gut, Gitea läuft schonmal. In einem letzten Schritt kümmere ich mich nun um den Action Runner.&lt;/p&gt;&#10;&lt;h3 id="action-runner-einrichten-"&gt;Action Runner einrichten 🏃&lt;/h3&gt;&#10;&lt;p&gt;Für die ersten Schritte hilft die offizielle &lt;a href="https://docs.gitea.com/usage/actions/act-runner" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; weiter.&lt;/p&gt;&#10;&lt;p&gt;Ich logge mich erneut auf der Weboberfläche über &lt;code&gt;git.schallbert.de&lt;/code&gt; ein. Die Aktivierung von Actions in der &lt;code&gt;app.ini&lt;/code&gt; im vorigen Abschnitt lässt jetzt bei &lt;code&gt;Settings-&amp;gt;Actions&lt;/code&gt; ein neues Menü erscheinen.&#10;Hier klicke ich nun auf &lt;code&gt;Create new Runner&lt;/code&gt; und kopiere mir das &lt;code&gt;REGISTRATION TOKEN&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Anschließend öffne ich die &lt;code&gt;docker-compose.yml&lt;/code&gt; im Gitea-Stammverzeichnis erneut und füge wie &lt;a href="https://docs.gitea.com/usage/actions/act-runner#set-up-the-runner-using-docker-compose" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hier vorgeschlagen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Folgendes ein:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;runner&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/act_runner:nightly&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;CONFIG_FILE=/config.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_INSTANCE_URL=https://git.schallbert.de&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_RUNNER_NAME=ichlaufe&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;GITEA_RUNNER_REGISTRATION_TOKEN= &amp;lt;redacted&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nach einem Neustart des Containers mit &lt;code&gt;docker compose down &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt; erscheint nun der Runner als &amp;ldquo;gestartet&amp;rdquo; im Log:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;✔ Network gitea_default Created 0.1s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;✔ Network gitea_gitea Created 0.0s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;✔ Container gitea Started 0.0s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;✔ Container gitea-runner-1 Started 0.1s &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hervorragend, jetzt schauen wir auf der Weboberfläche nach und siehe da:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/movetoownserver/actionrunner.jpg" alt="Image: Gitea action runner is up and - running."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Fehlt noch der Beweis, dass der Runner auch wirklich arbeitet.&lt;/p&gt;&#10;&lt;h3 id="funktionsprüfung-runner"&gt;Funktionsprüfung Runner&lt;/h3&gt;&#10;&lt;p&gt;Für die Prüfung des Action Runner nehme ich erneut die &lt;a href="https://blog.gitea.com/hacking-on-gitea-actions/#use-actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dokumentation von Gitea&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Hilfe.&#10;Ich erstelle ein neues Repository auf der Gitea-Weboberfläche mit Namen &lt;code&gt;runner-test&lt;/code&gt; und füge den Inhalt der oben verlinkten Datei im Verzeichnis &lt;code&gt;/.gitea/workflows/01-test.yml&lt;/code&gt; hinzu.&lt;/p&gt;&#10;&lt;p&gt;Commit, push, und - nichts.&lt;/p&gt;&#10;&lt;p&gt;Ah, der Workflow startet nur &lt;code&gt;on: [push]&lt;/code&gt;. Er kann also gar nicht starten, wenn er gerade selbst erst auf den Server geladen wurde.&lt;/p&gt;&#10;&lt;p&gt;Gut, erstelle ich halt noch eine Datei, commit, push. Ergebnis:&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/movetoownserver/actionsuccess.jpg" alt="Image: Gitea action runner demo passes integration"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;Wahnsinn! 🥳&lt;/p&gt;&#10;&lt;h2 id="zwischenfazit"&gt;Zwischenfazit&lt;/h2&gt;&#10;&lt;p&gt;Ich habe hier so getan, als wäre der Weg in einer geraden Linie verlaufen und als hätte alles reibungslos geklappt. Leider traf das so nicht zu. Es war zwar überraschend einfach, die Programme in Docker zum Laufen zu bringen, doch miteinander reden wollten sie lange nicht. Ich hatte zudem ziemliche Probleme mit der &lt;code&gt;Caddyfile&lt;/code&gt;&#10;So einfach sie auch erscheinen mag, lange wollte so gar nichts in meinem Browser erscheinen - weder Gitea noch die blog-Testseite. Ein Ping an die Seite vom Terminal des Servers aus funktionierte aber, also konnte ich den Fehler schnell auf die Konfiguration von caddy zurückführen.&lt;/p&gt;&#10;&lt;p&gt;Die Ursache: Da caddy im Docker läuft, muss die Caddyfile als Volume eingebunden werden. Im Caddyfile selber muss der Ordner vom Root des Volumes aus, also mit führendem &lt;code&gt;/&lt;/code&gt;, angegeben werden.&lt;/p&gt;&#10;&lt;p&gt;Auf der anderen Seite hat die Konfiguration von Gitea viel reibungsloser geklappt, als ich dachte. Sogar das aufwendige Freischalten des Runners, welcher auch im Docker nochmal extra geroutet werden muss, war weniger schlimm als ich dachte.&lt;/p&gt;&#10;&lt;p&gt;Mit diesem Grundgerüst habe ich jetzt einen flinken Server, welcher meinen Blog später bestimmt reibungslos hosten kann.&lt;/p&gt;&#10;&lt;p&gt;Die nächsten Schritte - das Aufsetzen des Bauprozesses für meine Website - werden &lt;a href="https://blog.schallbert.de/gitea-action-runner-native-jekyll/"&gt;in diesem Artikel hier&lt;/a&gt; und dann auch in Jekyll nativ &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/"&gt;hier&lt;/a&gt; abgehandelt.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: Server-Architektur&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-01-27T00:00:00Z"&gt;&#10; 27. Jan. 2024&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Ich habe den Server inzwischen mehrfach um Peripheriefunktionen erweitert. So habe ich mich um &lt;a href="https://blog.schallbert.de/server-protection/"&gt;Sicherheit und Backups&lt;/a&gt; ebenso gekümmert wie um &lt;a href="https://blog.schallbert.de/server-auto-upgrade/"&gt;automatische Updates&lt;/a&gt;, wobei mir die Kapselung durch Docker nicht nur einmal auf die Füße gefallen ist und einiges an Zeit gekostet hat. Auch wenn noch immer nicht alle Aspekte vollständig abgedeckt sind (Benachrichtigungen sind z.B noch ein offenes Thema), komme ich meinem Ziel, einen weitgehend selbstständig laufendes System zu schaffen, spürbar näher.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;p&gt;Am Ende möchte ich schließlich wieder dahin zurückkehren, Inhalte zu erzeugen. Und mich nicht mehr als nötig mit dem Drumherum beschäftigen.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;CI/CD steht für &amp;ldquo;&lt;a href="https://en.wikipedia.org/wiki/Continuous_integration" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Continuous Integration&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; / &lt;a href="https://en.wikipedia.org/wiki/Continuous_deployment" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Continuous deployment&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;rdquo; und bedeutet, dass das System bei Update der verwalteten Dateien automatisch die Website neu baut, testet und veröffentlicht.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;DNS steht für &lt;a href="https://en.wikipedia.org/wiki/Domain_Name_System" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Domain Name System&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, ein verteiltes hierarchisches System zur Namensauflösung von Websites. Hintergrund ist, dass es viel schwieriger ist, sich eine IPV6-Adresse wie &lt;code&gt;2a02:ec80:300:ed1a:0:0:0:1&lt;/code&gt; zu merken statt &lt;code&gt;en.wikipedia.org&lt;/code&gt; in die Adresszeile eines Browsers einzugeben.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item></channel></rss>