<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Software on Schallberts Blog</title><link>https://blog.schallbert.de/tags/software/</link><description>Recent content in Software on Schallberts Blog</description><generator>Hugo</generator><language>de</language><lastBuildDate>Sun, 06 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.schallbert.de/tags/software/index.xml" rel="self" type="application/rss+xml"/><item><title>Umzug von Jekyll zu Hugo</title><link>https://blog.schallbert.de/projects/migrating-jekyll-to-hugo/</link><pubDate>Sun, 06 Sep 2026 00:00:00 +0000</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 Minimal Mistakes zu Hugo (Theme Terminal).&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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/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="die-chancen"&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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; --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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;│   ├── audio&#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;│   ├── 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&#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;│   │   ├── navigation.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── ui-text.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;│   ├── navigation.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── ui-text.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;│   ├── archive.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── archive-taxonomy.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── categories.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── category.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── collection.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── compress.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── default.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── home.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── posts.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── search.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── single.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── splash.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── tag.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── tags.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── LICENSE&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── package.json&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── package-lock.json&#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;├── Rakefile&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── README.md&#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;span style="display:flex;"&gt;&lt;span&gt;└── tags&#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; │   └── tags.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── tags.md&#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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;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;│   └── scss&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── landing.scss &lt;span style="color:#75715e"&gt;# &amp;lt;-- custom styling for my landing page&lt;/span&gt;&#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;│   ├── 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;├── data&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── hugo.toml&#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;│   └── video.html&#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="hugo"&gt;Hugo&lt;/h3&gt;&#10;&lt;p&gt;Shortcodes werden 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-md" data-lang="md"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&amp;lt; shortcode-html-filename options &amp;gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&amp;lt; image src=&amp;#34;/path/to/image.avif&amp;#34; alt=&amp;#34;Image: alt text&amp;#34; position=&amp;#34;right&amp;#34; &amp;gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="jekyll"&gt;Jekyll&lt;/h3&gt;&#10;&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;div class="media-wrapper left"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;768px&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/_shortcodes/image.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;$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:#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;$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:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&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;$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:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;center&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;&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;image-wrapper &lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$position&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;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;$src&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;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;$alt&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;div&lt;/span&gt;&amp;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-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;image-wrapper&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;center&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;image-wrapper&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;left&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;image-wrapper&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;right&lt;/span&gt; &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:#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;border&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;solid&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;accent);&#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;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:#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;image-wrapper&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;left&lt;/span&gt; &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:#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;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;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;50&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;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;image-wrapper&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;right&lt;/span&gt; &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:#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;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;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;50&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;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="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;div class="media-wrapper center"&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-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_shortcodes/audio.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;$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:#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;$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; &lt;span style="color:#a6e22e"&gt;default&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;&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;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:#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;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:#f92672"&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:#f92672"&gt;source&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;$src&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;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&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; Your browser does not support the audio element.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&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:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$caption&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;media-title&amp;#34;&lt;/span&gt;&amp;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;&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;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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;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;media-wrapper&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;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;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;margin&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;line&lt;span style="color:#f92672"&gt;-&lt;/span&gt;height) &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;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;flex&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;justify-content&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;center&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-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;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;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;768&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; &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;border&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;solid&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;accent);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;background-color&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;accent);&#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:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;radius);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;overflow&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;hidden&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;text-align&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;center&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-container&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-container&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;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;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;}&#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-container&lt;/span&gt; &lt;span style="color:#f92672"&gt;iframe&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;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;min-height&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;400&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; &lt;span style="color:#66d9ef"&gt;aspect-ratio&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;9&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;}&#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-title&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;word-break&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;break-word&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;hyphens&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;color&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;background);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;font-weight&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;bold&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="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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Video-Beispiel (lokal): Linearführungen mit Kugelumlauf&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-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_shortcodes/video.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;$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:#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;$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:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&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;$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; &lt;span style="color:#a6e22e"&gt;default&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;&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;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:#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;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:#75715e"&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:#a6e22e"&gt;$src&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;http&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;iframe&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;$src&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;frameborder&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&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;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:#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; &amp;lt;&lt;span style="color:#f92672"&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:#f92672"&gt;source&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;$src&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;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&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; Your browser does not support the video tag.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&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:#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:#66d9ef"&gt;or&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$title&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$caption&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;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:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&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:#a6e22e"&gt;$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;$caption&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; &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;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="links"&gt;links&lt;/h3&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;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="direkter-vergleich"&gt;Direkter Vergleich&lt;/h2&gt;&#10;&lt;p&gt;Dann schauen wir uns jetzt 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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/figure&gt;&#10;&lt;/div&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;.&lt;/p&gt;&#10;&lt;h3 id="größe"&gt;Größe&lt;/h3&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.&lt;/p&gt;&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;/div&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.&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;p&gt;Insgesamt habe ich mit Hugo und Terminal deutliche Optimierungen vorliegen.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Manuell implementiert in Hugo hatte ich das bedingte Laden von Katex (&lt;code&gt;90kB min.css&lt;/code&gt;), Größenbeschränkungen für Medien-Vorschaudateien und insgesamt weniger CSS.&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 Terminal-Theme zeigt anders als minimalmistakes keine Kärtchen für &amp;ldquo;weitere Posts&amp;rdquo; an. Dies spart erneut die Übertragung von vier Vorschaubildern. Der Javascript-Inhalt geht außerdem gegen null - eines der Ziele, das ich mir gesteckt hatte.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="komplexität"&gt;Komplexität&lt;/h3&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 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;80%&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;Für die tagtägliche Benutzbarkeit gefällt mir Hugo ganz klar besser als Jekyll.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="auslieferungcontinuous-deployment"&gt;Auslieferung/Continuous Deployment&lt;/h3&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 gerne einmal &lt;code&gt;90 Sekunden&lt;/code&gt;, bis ein neuer Artikel live ist.&lt;/p&gt;&#10;&lt;p&gt;Für Hugo muss ich mal schauen, ob ich am Ende nicht doch einen eigenen Post mache&amp;hellip;&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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 00:00:00 +0000</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: An SSH public key randomart image as visual fingerprint for humans to quickly see if public keys match"&#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 00:00:00 +0000</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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&#10;&lt;/div&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 00:00:00 +0000</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;p&gt;⚠️ 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;)&lt;/p&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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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 00:00:00 +0000</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: Browser (Firefox) login process for passkey usage: PIN, then touch"&#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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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 zeitaufwändig 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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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 aufwändig 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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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>Vortrag: Software-defined vehicle</title><link>https://blog.schallbert.de/announcements/2026-01-25-lecture-sdv-available/</link><pubDate>Sun, 25 Jan 2026 00:00:00 +0000</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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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 00:00:00 +0000</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;/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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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 00:00:00 +0000</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;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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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 00:00:00 +0000</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;h2 id="-update-oktober-2025"&gt;⚠️ Update Oktober 2025&lt;/h2&gt;&#10;&lt;p&gt;Wird &lt;em&gt;fail2ban&lt;/em&gt; auf Services in Docker-Containern angewendet, kann mit der oben genannten Konfiguration des &lt;em&gt;jails&lt;/em&gt; Gein Fehler auftreten. Die Behebung habe ich im folgenden Artikel dokumentiert: &lt;a href="https://blog.schallbert.de/fail2ban-error-configuration-bad-value/"&gt;&lt;em&gt;fail2ban&lt;/em&gt;-Fehler &amp;ldquo;Bad value substitution&amp;rdquo; for &amp;lsquo;action&amp;rsquo;&lt;/a&gt;&lt;/p&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&gt;&#10;&lt;h2 id="-update-oktober-2025"&gt;⚠️ Update Oktober 2025&lt;/h2&gt;&#10;&lt;p&gt;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;ldquo;Bad value substitution&amp;rdquo; 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.&lt;/p&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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>Repo-Lookout: Sicherheitslücke beheben</title><link>https://blog.schallbert.de/repo-lookout-fix-deploy/</link><pubDate>Fri, 20 Dec 2024 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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>Giteas act_runner: Startprobleme</title><link>https://blog.schallbert.de/fix-gitea-runner/</link><pubDate>Fri, 30 Aug 2024 00:00:00 +0000</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;&amp;#39;s act_runner at startup"&#10; title="Giteas act_runner: Startprobleme" /&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 00:00:00 +0000</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="kommunikationsweg"&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 aufwändiger 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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;/div&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 00:00:00 +0000</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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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>CNC Tuning #3</title><link>https://blog.schallbert.de/cnc-tuning-pathinterpreter/</link><pubDate>Sat, 04 May 2024 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/cnc-tuning-pathinterpreter/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-20-G64RD-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumbnail of endmill cutting forces"&#10; title="CNC Tuning #3" /&gt;&#10;&lt;p&gt;Dies ist der letzte Teil meiner Reihe über die Optimierung der Fertigungszeit auf CNC-Fräsmaschinen. Er konzentriert sich auf den Pfadinterpreter der CNC-Steuersoftware.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/cnc-tuning-gcode/"&gt;&lt;em&gt;Teil 1&lt;/em&gt;: Besserer G-Code&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/cnc-tuning-kinematics/"&gt;&lt;em&gt;Teil 2&lt;/em&gt;: Kinematische Parameter hochdrehen&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Der Pfadinterpreter der Steuersoftware ist dafür zuständig, die Anweisungen aus dem &lt;code&gt;G-Code&lt;/code&gt; zu lesen und in Maschinenbewegungen zu übersetzen. Soll beispielsweise &lt;code&gt;G01 X100 Y200 Z-2.0 F2000 S18000&lt;/code&gt; ausgeführt werden, so muss der Pfadinterpreter die akuelle Position der Maschine kennen und einen Fräsvektor erzeugen, der zur gewünschten Position mit den vorgegebenen Parametern zeigt.&lt;/p&gt;&#10;&lt;p&gt;Hierbei muss der Interpreter die kinematischen Parameter der Maschine berücksichtigen: Mit welcher Rampe auf Zielgeschwindigkeit &lt;code&gt;F1000&lt;/code&gt; beschleunigt wird ob der gewünschte Vorschub überhaupt zulässig ist, und ab welchem Punkt wieder gebremst werden muss um den Zielpunkt exakt zu erreichen. Die so gewonnenen Daten werden schließlich in eine Schrittfolge für die drei Schrittmotoren übersetzt, in einen Pufferspeicher geladen und schließlich in Echtzeit an die Motorendstufen ausgegeben.&lt;/p&gt;&#10;&lt;h2 id="schritt-3-optimierung-des-pfadinterpreters"&gt;Schritt 3: Optimierung des Pfadinterpreters&lt;/h2&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-laf-settings.jpg" alt="Image: Look Ahead Feed in the CNC control software" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Viele Steuersoftware-Anbieter haben integrierte Fräspfadfilter programmiert, welche mittels Kommando &lt;code&gt;G64&lt;/code&gt; oder per Menü zu konfigurieren sind. Ich verlinke hier mal ein paar Beispiele verschiedener Hersteller, wo für spezifische Details nachgelesen werden kann:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://www.datron.com/resources/blog/toolpath-filter-achieves-a-perfect-surface-finish/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Toolpath filter&amp;rdquo; von Datron&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://docs.edingcnc.com/settings/trajectory" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Look Ahead Feed&amp;rdquo; von EdingCNC&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="http://wiki.linuxcnc.org/cgi-bin/wiki.pl?TrajectoryControl" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Path Blending&amp;rdquo; von LinuxCNC&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;Die Kunst ist nun, eine Konfiguration passend zum Fräsjob und der Maschinenkinematik zu finden. Es geht hier immer um einen Kompromiss zwischen Fertigungsgeschwindigkeit und Bahntoleranz. Soll also mit hohem Vorschub gefahren werden, werden eigentlich spitze Kanten verrundet, verbundene Liniensegmente in einem Toleranzband angefahren oder Pfade so vereinfacht, dass außerhalb der von der Maschine erreichbaren Beschleunigung befindliche Pfadpunkte nicht angefahren werden.&lt;/p&gt;&#10;&lt;h3 id="grundlagen-was-ist-look-ahead-feed"&gt;Grundlagen: Was ist &amp;ldquo;Look Ahead Feed&amp;rdquo;?&lt;/h3&gt;&#10;&lt;p&gt;Hiermit ist gemeint, dass der Pfadinterpreter der Steuersoftware sich während des Ausführens einer Fräsoperation - z.B. &lt;code&gt;G01 X* Y* (lineare Bewegung Punkt zu Punkt)&lt;/code&gt; - bereits die nächste(n) Operation(en) anschaut und herausfindet, ob und wie stark zwischen den Operationen abgebremst und wieder beschleunigt werden muss bzw. direkt durchgefahren werden kann. Die Physik dahinter erkläre ich &lt;a href="https://blog.schallbert.de/portal-milling-setup/#acceleration"&gt;im Artikel zum CNC-Setup&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Hierdurch kann teils massiv Zeit gespart werden, der Fräsvorgang wird flüssiger und die Maschine erfährt geringere Belastungen. Zusätzlich zur Vermeidung von Beschleunigungs- und Bremsvorgängen kann mittels Parameter &lt;code&gt;G64&lt;/code&gt; eingestellt werden, ob und wie Fräspfadfilter die geplante Trajektorie verändern dürfen, um weiter auf höhere Bahngeschwindigkeiten zu optimieren.&lt;/p&gt;&#10;&lt;h3 id="ideal-im-gravurbetrieb"&gt;Ideal im Gravurbetrieb&lt;/h3&gt;&#10;&lt;p&gt;Im Gravurbetrieb kann man sich solche Optionen hervorragend zu Nutze machen: Höhere Toleranzen sind hier meist OK, und leicht verrundete Kanten oft akzeptabel. Bei 3D-Gravuren dauert das Fräsen durch die vielen Pfade lang, und hier ist besonders viel Zeit herauszuholen.&lt;/p&gt;&#10;&lt;p&gt;Da ich Eding CNC benutze, kann ich nur für Parameter dieser Steuerung Tipps geben. Nun aber ab ins Detail zur Erklärung der Einstellungen!&lt;/p&gt;&#10;&lt;h3 id="fräspfadtoleranz-g64-p"&gt;Fräspfadtoleranz &lt;code&gt;G64 P*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-lafblending.jpg" alt="Image: Look Ahead Feed G64 CNC corner blending" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Dieser Parameter gibt der Steuersoftware die allgemeine Toleranz der Pfadgenauigkeit vor.&lt;/p&gt;&#10;&lt;p&gt;Ich habe meine Maschine so eingestellt, dass sie im Gravurbetrieb Abweichungen vom vorgegebenen Fräspfad von bis zu &lt;code&gt;LAF blending tolerance = 0.3mm&lt;/code&gt; zulässt. Alternativ kann die Konfiguration per Kommando &lt;code&gt;G64 P0.3&lt;/code&gt; erfolgen. Im Bild habe ich skizziert, wie sich eine solche Einstellung in der Praxis auswirken kann: Der Fräspfad (weiß) besteht aus &lt;code&gt;G01&lt;/code&gt;-Liniensegmenten. Die Toleranz ist mit Hilfe von Kreisen eingezeichnet. Der Pfadinterpreter der Steuersoftware kann nun eigenständig Kreissegmente so definieren, dass innerhalb der Toleranz eine möglichst hohe Bahngeschwindigkeit erreicht wird.&lt;/p&gt;&#10;&lt;p&gt;Die so ermittelte Bahn (türkis) ähnelt einer Spline-Kurve. Zu beachten ist hier, dass sämtliche Linien von dieser Einstellung betroffen sein können - auch solche, die strenge Toleranzanforderungen haben. Ist dies nicht gewünscht, so sollte hier ein kleinerer Wert verwendet werden.&lt;/p&gt;&#10;&lt;h3 id="pfadverrundung-g64-r"&gt;Pfadverrundung &lt;code&gt;G64 R*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Treffen zwei Liniensegmente im Winkel von bis zu &lt;code&gt;LAF full speed blending angle threshold = 12°&lt;/code&gt; aufeinander, wird die Maschine ohne abzubremsen versuchen, dem Winkel genau zu folgen. Bei größeren Winkeln wird mit reduzierter Geschwindigkeit mit per &lt;code&gt;G64 P*&lt;/code&gt; definierter Toleranz gefahren. Diese Konfiguration wird mit &lt;code&gt;G64 R12&lt;/code&gt; programmiert.&lt;/p&gt;&#10;&lt;p&gt;Die Werte hier sind mit großer Vorsicht einzustellen, weil sie zu Beschleunigungsspitzen auf der Maschine führen. Große Werte können Schrittverluste und höheren Verschleiß der CNC-Fräse verursachen. Dafür werden Bahngeschwindigkeit und Genauigkeit optimal gehalten.&lt;/p&gt;&#10;&lt;h3 id="pfadverrundung-bei-reduziertem-vorschub-g64-s-d"&gt;Pfadverrundung bei reduziertem Vorschub &lt;code&gt;G64 S* D*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-G64RD.jpg" alt="Image: Look Ahead Feed G64 CNC corner radius" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Diese Konfiguration gibt der Steuersoftware die Möglichkeit, Kanten bis zum eingestellten Toleranzwert zu verrunden: &lt;code&gt;LAF reduced speed path smoothing delta = 0.001mm&lt;/code&gt;. In Kombination mit der Verrundung muss der Winkel eingestellt werden, bis zu dem diese Verrundung durchgeführt werden soll: &lt;code&gt;LAF reduced speed path smoothing angle threshold = 0°&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Programmiert wird diese Funktion am Besten in Kombination mit der Pfadverrundung bei vollem Vorschub als &lt;code&gt;G64 R* S* D*&lt;/code&gt;. Der Vorteil hierbei ist, dass Liniensegmenten wie im Bild gezeigt sehr genau gefolgt wird, und die Kanten gleichzeitig aber fein justierbar abgerundet werden. Bis zum über &lt;code&gt;R&lt;/code&gt; definierten Winkel wird sogar mit vollem Vorschub dem Pfad exakt gefolgt.&lt;/p&gt;&#10;&lt;p&gt;Werden bei &lt;code&gt;S&lt;/code&gt; sehr große Winkel eingestellt, so findet entsprechend &lt;code&gt;D&lt;/code&gt; auch eine stärkere Verrundung statt. Überschreitet der im Fräsprogramm vorgegebene Winkel jedoch den konfigurierten Wert, so fällt der Algorithmus auf die für &lt;code&gt;P&lt;/code&gt; hinterlegten Werte bzw. in den exakten Pfadfolgemodus zurück.&lt;/p&gt;&#10;&lt;h3 id="pfadvereinfachung-g64-q"&gt;Pfadvereinfachung &lt;code&gt;G64 Q*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-lafcombine.jpg" alt="Image: Look Ahead Feed G64, path combine setting" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Enthält die G-Code Datei zahlreiche, sehr kurze Liniensegmente (wie sie oft bei der Erstellung von Pfaden aus Bilddateien entstehen), kann diese Funktion durch Vereinfachen der Fräspfade Abhilfe schaffen. Dabei gibt die Zahl hinter dem Parameter die Toleranz an, bis zu der Liniensegmente zu einer Linie verschmolzen werden.&lt;/p&gt;&#10;&lt;p&gt;Diese Einstellung entlastet den Pufferspeicher der Maschine und erhöht gleichzeitig die mittlere Bahngeschwindigkeit. Für den Gravurbetrieb halte ich diese Einstellung insbesondere für interessant, wenn mit &amp;ldquo;verrauschten&amp;rdquo; Daten gearbeitet wird und im CAD nicht die Möglichkeit bestand, die Anzahl der Pfade zu reduzieren.&lt;/p&gt;&#10;&lt;h3 id="beschleunigungsfilter-g64-f"&gt;Beschleunigungsfilter &lt;code&gt;G64 F*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Mittels diesen Parameters können die durch Look Ahead Feed möglicherweise erzeugten Beschleunigungsspitzen abgemildert werden. Der minimal einzustellende Wert ergibt sich durch den Quotienten aus gewähltem Vorschub für die Operation und maximaler Geschwindigkeit der Maschine.&lt;/p&gt;&#10;&lt;p&gt;Beispiel: &lt;code&gt;Fmin = Foperate / Fg00&lt;/code&gt;, bei &lt;code&gt;Fg00 = 12000mm/min&lt;/code&gt; und &lt;code&gt;Foperate = 5000mmm/min&lt;/code&gt; ist &lt;code&gt;Fmin = 0.42&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Also wird bei Vorschub von &lt;code&gt;5000mm/min&lt;/code&gt; die maximal mögliche Beschleunigung der Maschine abgerufen und Kurvenradien so eingestellt, dass sie bei konstanter Bahngeschwindigkeit gefahren werden. Alle Werte größer gleich &lt;code&gt;Fmin = 1&lt;/code&gt; sind inherent sicher, unabhängig vom gewählten Vorschub.&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-accelfilter.jpg" alt="Image: acceleration filter for G64 operation" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Dieser Parameter wurde entwickelt für die Bearbeitung von elastischen Materialien wie Gummi oder Kork, wo eine konstante Vorschubgeschwindigkeit extrem wichtig und die Bahntreue eher zweitrangig ist. Der Parameter wird in Kombination mit &lt;code&gt;G64 R*&lt;/code&gt; verwendet. Die folgende Programmierung für das obige Beispiel würde die volle Kapazität der Maschine abrufen: &lt;code&gt;G64 R120 F0.42&lt;/code&gt;, wobei mit &lt;code&gt;F5000&lt;/code&gt; gefahren wird, &lt;code&gt;F12000&lt;/code&gt; der Maximalvorschub der Maschine ist und spitze Winkel bis &lt;code&gt;120°&lt;/code&gt; ohne abzubremsen gefahren werden sollen.&lt;/p&gt;&#10;&lt;p&gt;Wende ich diese Werte auf meine Maschine an, entsteht das nebenstehende Bild. Hier soll ein Rechteck mit &lt;code&gt;25 x 100mm&lt;/code&gt; Kantenlänge gefertigt werden. Weiß ist wieder die Zieltrajektorie dargestellt. Die türkise Kurve zeigt die durch die Programmierung des Beschleunigungsfilters gefahrene Kurve mit konstanter Bahngeschwindigkeit. Je geringer der Vorschub im Verhältnis zum Maximalvorschub und je höher die Beschleunigung der Maschine eingestellt ist, desto kleiner wird der notwendige Radius.&lt;/p&gt;&#10;&lt;p&gt;Für den Gravurbetrieb und das Fräsen fester Materialien wird dieser Parameter meiner Erfahrung nach jedoch nicht verwendet.&lt;/p&gt;&#10;&lt;h2 id="meine-konfiguration-für-die-hobbyline-4530"&gt;Meine Konfiguration für die Hobbyline 4530&lt;/h2&gt;&#10;&lt;p&gt;Ich habe im Gravurbetrieb viel mit den verschiedenen LAF-Parametern herumgespielt und eine Menge Rohlinge als Lehrgeld &amp;ldquo;verbraucht&amp;rdquo;. Die kürzesten Fertigunszeiten erhielt ich bei vorheriger Optimierung der kinematischen Parameter mit folgender Konfiguration:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;code&gt;G64 Q0.3 R12 D0.3 S120&lt;/code&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Damit sage ich der Maschine folgendes:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Verwende den Pfadvorschau-Algorithmus und versuche, kurze Liniensegmente unter &lt;code&gt;0.3mm&lt;/code&gt; Länge zu verschmelzen. Versuche, den Vorschub bei einem Winkel zweier Streckenabschnitte von bis zu &lt;code&gt;12°&lt;/code&gt; nicht herunterzuregeln. Bei größeren Winkeln bis &lt;code&gt;120°&lt;/code&gt; kann statt einem exakten Nachfahren mit bis zu &lt;code&gt;r=0.3mm&lt;/code&gt; verrundet werden. Bei noch größeren Winkeln ist dem vorgegebenen Pfad zu folgen und, falls erforderlich, bis zum Stillstand abzubremsen.&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="fazit"&gt;Fazit&lt;/h2&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-duplicated-software.jpg" alt="Image: dual install of CNC control software for different purposes" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Da mich die Parameteroptimierung für den Gravurbetrieb ziemlich weit vom Parametersatz für den normalen Fräsbetrieb weggebracht haben, habe ich mein Steuerprogramm einfach ein zweites Mal installiert.&lt;/p&gt;&#10;&lt;p&gt;Dort konnte ich dann Geschwindigkeit, Achsbeschleunigung und Pfadparametrierung getrennt festlegen. Nun habe ich das Beste beider Welten auf einer Maschine zur Verfügung:&lt;/p&gt;&#10;&lt;p&gt;Einen sehr genauen, weich operierenden Fräsbetrieb mit folgenden Parametern:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;Fmax XY 12000mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Fmax Z 4800mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax X 1000mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax Y 1000mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax Z 1000mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;G64 P0.1 R6&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Und über die andere Verknüpfung einen überaus flinken Gravurbetrieb:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;Fmax XY 12000mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Fmax Z 4800mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax X 4000mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax Y 3200mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax Z 1600mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;G64 Q0.3 R12 D0.3 S120&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="und-wo-ist-der-beweis"&gt;Und wo ist der Beweis?&lt;/h2&gt;&#10;&lt;p&gt;Das eingebettete Video zeigt dasselbe Gravurprojekt mit vier verschiedenen Optimierungsstufen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;oben links: Herstellerkonfiguration der Maschine, Standard-Fräspfade aus CAD/CAM&lt;/li&gt;&#10;&lt;li&gt;oben rechts: Geschwindigkeit und Beschleunigung auf Fräsbetrieb erhöht&lt;/li&gt;&#10;&lt;li&gt;unten links: Look Ahead feed eingeschaltet &lt;code&gt;G64 P0.3&lt;/code&gt;, Fräspfade im CAM optimiert&lt;/li&gt;&#10;&lt;li&gt;unten rechts: Optimiert für Gravurbetrieb wie oben beschrieben, CAM Flyover auf &lt;code&gt;2mm reduziert&lt;/code&gt; und Leerfahrten minimiert&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;iframe src="https://makertube.net/videos/embed/2EWe1ZvVW9TKY6k3XtpmWF" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;CNC-Tuning für den Gravurbetrieb&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>CNC Tuning #2</title><link>https://blog.schallbert.de/cnc-tuning-kinematics/</link><pubDate>Sat, 27 Apr 2024 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/cnc-tuning-kinematics/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-kinematics-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumbnail of a CNC machine&amp;#39;&amp;#39;s portal X-Axis"&#10; title="CNC Tuning #2" /&gt;&#10;&lt;p&gt;Dies ist der zweite Teil meiner Reihe über die Optimierung der Fertigungszeit auf CNC-Fräsmaschinen. Er konzentriert sich auf die kinematischen Maschinenparameter für den Gravurbetrieb.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/cnc-tuning-gcode/"&gt;&lt;em&gt;Teil 1&lt;/em&gt;: Besserer G-Code&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/cnc-tuning-pathinterpreter/"&gt;&lt;em&gt;Teil 3:&lt;/em&gt; Pfadinterpreter optimieren&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="schritt-2-tuning-der-kinematischen-parameter-der-cnc"&gt;Schritt 2: Tuning der kinematischen Parameter der CNC&lt;/h2&gt;&#10;&lt;p&gt;Die theoretischen Grundlagen habe ich im &lt;a href="https://blog.schallbert.de/portal-milling-setup/"&gt;Artikel zur CNC-Steuerungskonfiguration&lt;/a&gt; ausführlich erläutert. Meine Probleme damit und zugehörige Lösungen habe ich nach vielen Tests und Rücksprache mit dem Hersteller im &lt;a href="https://blog.schallbert.de/projects/one-year-zerspanobert/#velocityerror"&gt;Fazit zu einem Jahr CNC-Betrieb&lt;/a&gt; festgehalten. Die folgenden Abschnitte wenden die obigen Erkenntnisse auf eine Demonstrationsmaschine an, die ich mir habe leihen können.&lt;/p&gt;&#10;&lt;h3 id="verfahrgeschwindigkeit-im-eilgang"&gt;Verfahrgeschwindigkeit im Eilgang&lt;/h3&gt;&#10;&lt;p&gt;Für die Demomaschine konnte ich die Geschwindigkeit im Eilgang von den vorgeschlagenen &lt;code&gt;Fxy = 4m/min&lt;/code&gt; und &lt;code&gt;Fz = 2.4m/min&lt;/code&gt; auf &lt;code&gt;Fxy = 12m/min&lt;/code&gt; und &lt;code&gt;Fz = 4.8m/min&lt;/code&gt; erhöhen. Setze ich alle drei Achsen gleichzeitig in Bewegung, erhalte ich durch Steigung der Kugelgewindespindeln und Mikroschrittkonfiguration (8 pro Vollschritt) der Motoren eine kombinierte Schrittfrequenz von &lt;code&gt;96kHz&lt;/code&gt;, was unter der maximalen Schrittfrequenz von &lt;code&gt;125kHz&lt;/code&gt; der Steuerung liegt.&lt;/p&gt;&#10;&lt;h3 id="beschleunigung-im-eilgang"&gt;Beschleunigung im Eilgang&lt;/h3&gt;&#10;&lt;p&gt;Die Herstellerangaben empfehlen hier &lt;code&gt;aXY = 300mm/s²&lt;/code&gt; und &lt;code&gt;aZ = 200mm/s²&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Ich habe die Beschleunigung in mehreren Schritten hochgesetzt und bin in einer ersten Runde bei &lt;code&gt;aXY = 1000mm/s²&lt;/code&gt; und &lt;code&gt;aZ = 400mm/s²&lt;/code&gt; gelandet. Diese Werte sind für diese kleine und leichte Maschine mit nur &lt;code&gt;30x45x14cm&lt;/code&gt; Verfahrweg nach meiner Erfahrung für den Normalbetrieb ohne Weiteres möglich. Sie erhöhen die Fertigungsgeschwindigkeit stark, denn jeder Richtungswechsel, auch die im Materialeingriff bei &lt;code&gt;G01, G02, G03&lt;/code&gt;, werden nun um Faktor &lt;code&gt;3&lt;/code&gt; bzw. &lt;code&gt;2.5&lt;/code&gt; schneller ausgeführt.&lt;/p&gt;&#10;&lt;p&gt;So habe ich die Maschine oft betrieben und auch auf der Messe bin ich mit diesen Werten stabil und sicher gefahren. Das folgende Video zeigt die Fräse im Gravurbetrieb mit den angegebenen Werten.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;iframe src="https://makertube.net/videos/embed/vvmwT8e6vWnj5zbuqMdUfN" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;CNC-Tuning: Vorschau&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Danach jedoch wollte ich wissen, wie weit ich es treiben kann. Getreu dem vorher beschriebenen Motto&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Wer die Grenze nicht überschreitet, weiß nicht, wo sie liegt&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;habe ich erst einmal geprüft, wie sich massive Schrittverluste bei dieser Maschine anhören. Also habe ich eine Achse mit einem Holzbalken blockiert und bin vorsichtig dagegen gefahren:&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-04-27-soundoflosingsteps.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Massive step loss soundcheck&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Das deutlich vernehmbare Rattern entsteht wenn der Motorhochlauf zwar klappt, in Richtung Zielgeschwindigkeit aber diverse Vollschritte verloren gehen. Wird der Rotor bereits bei Anlauf des Motors &amp;ldquo;verloren&amp;rdquo;, so fiept der Motor nur noch, ohne die Achse überhaupt in Bewegung zu setzen. Verliert der Motor nur wenige Schritte, befindet sich aber auf Zielgeschwindigkeit, so gibt es einen rauen und unter Umständen kratzenden Ton.&lt;/p&gt;&#10;&lt;p&gt;Ich habe für das Experiment die X-Achse gewählt, weil sie sich am einfachsten blockieren lässt. Des Weiteren habe ich sie in &lt;code&gt;X+&lt;/code&gt;-Richtung blockiert. Begründung: diese Maschine besitzt kein professionelles Festlager zur Aufnahme von Druck- und Zugkräften der Kugelumlaufspindel. Stattdessen werden Druckkräfte in Richtung Schrittmotor sicher von einem auf der Spindel montierten Flansch abgefangen, der gegen in der Portalwange befindliche Kugellager drückt.&lt;/p&gt;&#10;&lt;p&gt;Zugkräfte hingegen werden hauptsächlich von den Motorlagern aufgenommen - da wollte ich kein Risiko eingehen.&#10;Nun ist also das Geräusch bei plötzlichem und ruckartigen Schrittverlust bekannt - das Übertakten kann beginnen!&lt;/p&gt;&#10;&lt;p&gt;Meine Vorgehensweise:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Die Beschleunigung einer Achse der Maschine verdoppeln&lt;/li&gt;&#10;&lt;li&gt;Die betreffende Achse umher joggen, dabei auf merkwürdige Geräusche achten und die Achsbewegung beobachten&lt;/li&gt;&#10;&lt;li&gt;Vorige Schritte wiederholen, bis massive Schrittverluste auftreten&lt;/li&gt;&#10;&lt;li&gt;Beschleunigung auf den Mittelwert zwischen &amp;ldquo;klappt&amp;rdquo; und &amp;ldquo;klappt nicht&amp;rdquo; zurückfahren&lt;/li&gt;&#10;&lt;li&gt;Den vorigen Schritt solange wiederholen, bis die Maschine sauber fährt&lt;/li&gt;&#10;&lt;li&gt;Alle Schritte auch für die anderen Achsen durchführen&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;So bin ich schließlich bei für eine solche Hobbymaschine unglaublichen &lt;code&gt;aXY = 4000mm/s²&lt;/code&gt;, &lt;code&gt;aZ = 1600mm/s²&lt;/code&gt; gelandet. Dies liegt nun eine Größenordnung über den empfohlenen Werten.&lt;/p&gt;&#10;&lt;p&gt;Die kleine Fräse scheint auch hier stabil zu laufen und die gefertigten Gravuren sehen gut aus. Allerdings konnte ich mir nicht vorstellen, dass ich Schrittmotoren, Treiberstufen und Netzteil noch innerhalb ihrer Spezifikation betreibe. Daher habe ich gemessen.&lt;/p&gt;&#10;&lt;h3 id="elektrische-messwerte"&gt;Elektrische Messwerte&lt;/h3&gt;&#10;&lt;p&gt;Das Netzteil ist mit &lt;code&gt;3.7A&lt;/code&gt; bei &lt;code&gt;36V&lt;/code&gt; angegeben. Ich darf diesen Wert also in der Summe weder im Eilgang &lt;code&gt;G00&lt;/code&gt; noch im Materialeingriff dauerhaft überschreiten. Wie oben erklärt, ist der Eilgang im Gravurbetrieb durch die geringeren Fräskräfte maßgeblich, sodass ich besonderes Augenmerk auf das Verfahren mehrerer Achsen gleichzeitig legen muss.&lt;/p&gt;&#10;&lt;p&gt;Die Motoren können je einen Dauerstrom von &lt;code&gt;3.0A&lt;/code&gt; verkraften, limitieren mir hier also nichts. In den elektrischen Tests schaue ich lediglich, ob ich mit der Energieaufnahme des Systems im Rahmen bleibe.&lt;/p&gt;&#10;&lt;h4 id="messaufbau"&gt;Messaufbau&lt;/h4&gt;&#10;&lt;p&gt;Ich schließe das aus &lt;a href="https://blog.schallbert.de/measure-inrush-current/"&gt;diesem Artikel bekannte&lt;/a&gt; Multimeter direkt am Ausgang des Netzteils an und nehme Spannung und Strom auf. Für das Experiment simuliere ich die Fertigung der oben im Video verlinkten Gravur und führe in zwei Durchläufen zuerst eine zeitlich grob aufgelöste Leistungsfaktoranalyse (&lt;code&gt;2Hz&lt;/code&gt;) durch, gefolgt von einer fein aufgelösten Stromermittlung mit &lt;code&gt;20kHz&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h4 id="ergebnisse"&gt;Ergebnisse&lt;/h4&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-powerdiagram-engrave.jpg" alt="Image: Power diagram for tuned engraving" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Im obigen Bild sind Spannung, Strom, Wirk- und Blindleistung aufgetragen. Zu erkennen ist, dass die Blindleistung (&lt;code&gt;grün&lt;/code&gt;) immer dann kurze Spitzen zeigt, wenn mehrere Achsen gleichzeitig in den Eilgang beschleunigen. Die Leistungsaufnahme einer Achse im Eingriff &lt;code&gt;G01/G02/G03, blau&lt;/code&gt; beträgt im Schnitt etwa &lt;code&gt;45W&lt;/code&gt; und ist ähnlich hoch wie bei der Konstantfahrt im Eilgang.&lt;/p&gt;&#10;&lt;p&gt;Das Netzteil ist in der Lage, die Spannung (&lt;code&gt;orange&lt;/code&gt;) ziemlich konstant zu halten und knickt selbst unter Volllast nur um knapp &lt;code&gt;0.2V&lt;/code&gt; ein. Einzig der Stromwert sah mir etwas zu glatt aus. Ich vermutete an dieser Stelle, dass die zeitliche Auflösung zu gering ist und beschloss, das Experiment mit Konzentration auf den Strom zu wiederholen.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-currentdiagram-engrave.jpg" alt="Image: Current diagram for tuned engraving" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Und tatsächlich. Die höhere zeitliche Auflösung zeigt, dass die Stromaufnahme bei Beschleunigung mehrerer Achsen Spitzen zeigt, die über den bereits bekannten Werten liegt.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-currentpeak-engrave.jpg" alt="Image: Zoomed-in current draw for axis acceleration" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Hier stechen Bewegungen auf der Y-Achse, welche das gesamte Portal in Bewegung setzen muss, besonders heraus. Und zwar genau dann, wenn die anderen Achsen noch in Bewegung sind oder gerade gehalten haben. Dies hängt mit den Schrittmotortreibern zusammen. Sie betreiben die Motoren nämlich noch etwa &lt;code&gt;0.5s&lt;/code&gt; nach Anhalten bei vollem Strom weiter. Die dadurch entstehende Überlagerung lässt die Spitzen noch dramatischer aussehen.&lt;/p&gt;&#10;&lt;h4 id="benchmark-spitzenlast"&gt;Benchmark Spitzenlast&lt;/h4&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-loadtest.jpg" alt="Image: Zoomed-in current draw for axis acceleration" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Na dann treiben wir es mal auf die Spitze mit einem Beschleunigungs-Benchmark. Wird das Netzteil auch bei voller Last unter Beschleunigung aller Motoren gleichzeitig die Ausgangsspannung halten können?&lt;/p&gt;&#10;&lt;p&gt;Um dies zu testen, habe ich einen G-Code geschrieben, der alle Achsen gleichzeitig auf Höchstgeschwindigkeit beschleunigen und dann wieder abbremsen lässt. Anschließend wird unter gleichen Bedingungen zum Ausgangspunkt zurückgefahren und das Ganze wiederholt.&lt;/p&gt;&#10;&lt;p&gt;Auf diese Weise kann ich herausfinden, ob das Netzteil genügend Reserven hat, um Lastspitzen abzufangen und gleichzeitig schauen, ob die Motoren bereits im größeren Maße Schritte verlieren. Im Mittel liegt hier die aufgebrachte Leistung bei &lt;code&gt;95W&lt;/code&gt; und die Versorgungsspannung sinkt um &lt;code&gt;0.28V&lt;/code&gt; auf &lt;code&gt;35.72V&lt;/code&gt; ab. Dabei muss das Netzteil in der Spitze &lt;code&gt;5.14A&lt;/code&gt; abgeben, was knapp &lt;code&gt;40%&lt;/code&gt; über Nennstrom liegt.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-04-20-benchmark.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Benchmark&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Hört man ganz genau hin, so klingt die Maschine direkt zu Beginn des Benchmarks etwas rau, fängt sich dann aber. So können auch Schrittverluste klingen. Es ist daher sehr wichtig, die Maschine mit allen Sinnen zu überwachen.&lt;/p&gt;&#10;&lt;h3 id="mechanische-prüfung"&gt;Mechanische Prüfung&lt;/h3&gt;&#10;&lt;p&gt;Nun zur letzten Prüfung: Ich gebe beim Eilgang-Benchmark zusätzlich Last auf die Achsen, indem ich während des Programmablaufes mit meinem Arm an der jeweiligen Achse ziehe. Die Achsen Z und X laufen stabil weiter. Bei der Portalachse &lt;code&gt;Y&lt;/code&gt; merke ich jedoch, dass die Maschine nun anders klingt. Irgendwie rau, als würde man mit mit einer Raspel leicht über eine Holzplatte ziehen.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-steplossy.jpg" alt="Image: Y-axis reveals massive step loss" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Lege ich eine große Last an, so fährt der Schrittmotor auf dieser Achse nicht mal mehr an, sondern fiept nur noch. Er kann den Pulsen der Endstufe nicht mehr folgen. Ich reduziere nun also die Beschleunigung schrittweise, bis der Motor die Portalachse auch unter mittlerer Last wiederholbar und ohne hörbare Schrittverluste beschleunigen kann. Hier lande ich am Ende bei immer noch flinken &lt;code&gt;[aX = 4000mm/s² aY = 3200mm/s² aZ = 1600mm/s²]&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;In einem letzten Schritt führe ich nach fünfmaliger Wiederholung des Benchmarks eine weitere Referenzfahrt durch und schaue mir die Logs der Steuersoftware an. Alle Achsen sind nun unauffällig hinsichtlich Schrittverlusten.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-steplossxz.jpg" alt="Image: XZ-axis without step loss" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="appelle-zum-cnc-tuning"&gt;Appelle zum CNC-Tuning&lt;/h3&gt;&#10;&lt;p&gt;Bitte geht beim Tuning eurer Maschine behutsam vor. Lernt sie richtig kennen, bevor ihr damit beginnt. Hört genau hin und optimiert Achse für Achse nacheinander. Nehmt euch Zeit für Tests, Prüfungen und Experimente.&lt;/p&gt;&#10;&lt;p&gt;Selbst wenn ihr eine sehr ähnliche Maschine wie meine besitzt, machen andere Abmessungen bereits einen riesigen Unterschied hinsichtlich Steifigkeit und damit auch möglicher Verfahrgeschwindigkeit bzw. Beschleunigung. Sogar Wartungszustand, leichtgängigere Wellen, andere Führungen o.Ä. können dazu führen, dass ihr am Ende mit anderen Werten fahrt als ich.&lt;/p&gt;&#10;&lt;p&gt;Es wird kein Profi-Equipment benötigt, um die Grenzen der Maschine auszuloten. Nicht einmal eine Messuhr, da Schrittverluste auch mit Hilfe der Referenzschalter hinreichend genau bestimmt werden können.&lt;/p&gt;&#10;&lt;p&gt;Alles was ich durch die elektrischen Messungen oben festgestellt habe, ist, dass der Hersteller bei der Auslegung der Maschine sauber gearbeitet hat und keine Komponente unterdimensioniert ist. Im Gegenteil, diese Maschine ist für die Übertaktung bestens geeignet.&lt;/p&gt;&#10;&lt;p&gt;Und nun weiter zum letzten &lt;a href="https://blog.schallbert.de/cnc-tuning-pathinterpreter/"&gt;&lt;em&gt;Teil 3:&lt;/em&gt; Pfadinterpreter optimieren&lt;/a&gt;.&lt;/p&gt;&#10;</description></item><item><title>CNC Tuning #1</title><link>https://blog.schallbert.de/cnc-tuning-gcode/</link><pubDate>Sat, 20 Apr 2024 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/cnc-tuning-gcode/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-20-camtuned-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumbnail of CAM simulation for G-code optimizing"&#10; title="CNC Tuning #1" /&gt;&#10;&lt;p&gt;Dies ist der erste Teil meiner Reihe über die Optimierung der Fertigungszeit auf CNC-Fräsmaschinen. Er konzentriert sich auf den Einsatz von CAD- und CAM-Werkzeugen zur Erzeugung effizienteren G-Codes.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/cnc-tuning-kinematics/"&gt;&lt;em&gt;Teil 2&lt;/em&gt;: Kinematische Parameter hochdrehen&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/cnc-tuning-pathinterpreter/"&gt;&lt;em&gt;Teil 3:&lt;/em&gt; Pfadinterpreter optimieren&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Es gibt diverse Maßnahmen zur Reduktion der Fertigungszeit mit einer CNC-Maschine. Erstmal jedoch sollte beleuchtet werden, warum so etwas gewünscht ist und welche möglichen Nachteile sich hierdurch ergeben.&lt;/p&gt;&#10;&lt;h2 id="warum-überhaupt"&gt;Warum überhaupt?&lt;/h2&gt;&#10;&lt;p&gt;Aus kaufmännischer Sicht sorgen geringere Fertigungszeiten für einen höheren Teiledurchsatz und ermöglichen damit einen höheren Umsatz in einer gegebenen Zeitspanne. Zudem ist die Investition der Maschine schneller amortisiert.&lt;/p&gt;&#10;&lt;p&gt;Als Hobbyist sind kürzere Fertigungszeiten vor allem dann interessant, wenn das Ziel die Herstellung sehr komplexer Geometrien oder die 3D-Fertigung ist. Hier können Fräszeiten durchaus mehrere Stunden betragen und die möglichen Einsparungen sind folglich besonders groß.&lt;/p&gt;&#10;&lt;p&gt;Bei mir steht wie auch in der Lehre der wissenschaftliche Gedanke im Vordergrund: Ich will herauszufinden, wo die Grenze der Maschine liegt. Ich möchte wissen, warum sie dort liegt. Ich werde versuchen, sie stabil innerhalb ihrer Grenzen zu betreiben. Dabei ist der Verschleiß unter Kontrolle zu halten.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Um diese Grenze jedoch genau zu kennen, muss sie zuerst übertreten werden. Woher sonst soll ich wissen, wo sie sich befindet?&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="was-sind-mögliche-nachteile"&gt;Was sind mögliche Nachteile?&lt;/h3&gt;&#10;&lt;p&gt;Bestimmte Komponenten einer &amp;ldquo;übertakteten&amp;rdquo; Maschine könnten schneller als üblich verschleißen. Dazu zählen etwa Netzteile und Motor-Endstufen, die höhere transiente Ströme verkraften müssen. Lager und Führungen werden durch höhere Beschleunigungen stärker beansprucht. Umlaufspindeln und Maschinenrahmen müssen die dadurch resultierenden, stärkeren und schneller wechselnden Kräfte und Vibrationen aufnehmen und können früher Ermüdungserscheinungen oder sich lösende Verbindungen aufweisen.&lt;/p&gt;&#10;&lt;p&gt;Bei der Ermittlung der Grenzen kann die Maschine im Extremfall sogar beschädigt werden: Mit hoher Wahrscheinlichkeit treten jenseits der Grenze Schrittverluste&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; auf. Das ist zwar im Testbetrieb nicht weiter problematisch. Doch stimmt die angenommene Position dann nicht mehr mit der tatsächlichen Position überein und es besteht das Risiko des &amp;ldquo;Crashes&amp;rdquo; - wenn nicht mit Bedacht vorgegangen wird. Hier noch ein ziemlich gutes &lt;a href="https://www.youtube.com/watch?v=C_9dM1wx0Sg" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Youtube-Video von &lt;em&gt;gammaflow&lt;/em&gt; zum Thema Schrittverluste vermeiden&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Werden die Fräsbahnen im CAD oder CAM zugunsten kürzerer Fertigungszeiten verändert, so kann dies außerdem negative Auswirkungen auf Schnitt-, Kanten- und Oberflächenqualität haben.&lt;/p&gt;&#10;&lt;p&gt;Bei der Optimierung des Pfadinterpreters der Steuersoftware ist ebenfalls ein Kompromiss einzugehen: Höhere Fertigungsgeschwindigkeit wird hier mit geringerer Genauigkeit und/oder höherem Maschinenverschleiß eingekauft.&lt;/p&gt;&#10;&lt;h3 id="welche-maßnahmen-werden-nicht-diskutiert"&gt;Welche Maßnahmen werden nicht diskutiert?&lt;/h3&gt;&#10;&lt;p&gt;Nicht betrachten werde ich hier die Optimierung von Vorschub &lt;code&gt;F&lt;/code&gt; und Drehzahl &lt;code&gt;S&lt;/code&gt; des gewählten Fräsers im Eingriff. Diese Parameter hängen vom zu bearbeitendem Material, der Fräsergeometrie, den Fähigkeiten der Maschine und dem Mut des Bedienpersonals ab, sodass ich keine allgemein gültigen Tunings vorschlagen kann.&lt;/p&gt;&#10;&lt;p&gt;Einen anderen, großen Einflussfaktor muss ich hier ebenfalls ausklammern: Die Bearbeitungsstrategie des CAM-Programms bezogen auf Eilgänge. Mein CAM-Programm lässt mich da nicht besonders tief eingreifen, sodass ich für die Optimierung der letzten paar Prozent Fertigungszeit stets von Hand im G-Code arbeiten muss, was entsprechend fehleranfällig ist.&lt;/p&gt;&#10;&lt;h2 id="tuning-in-drei-schritten"&gt;Tuning in drei Schritten&lt;/h2&gt;&#10;&lt;p&gt;Bei meiner Optimierung bin ich in drei Schritten vorgegangen, die allerdings unabhängig voneinander umgesetzt werden können. Jeder Schritt hat mir hierbei deutlich spürbare Verbesserungen gebracht. Dieser Artikel konzentriert sich aus mehreren Gründen auf den Gravurbetrieb:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Gravuren erlauben meist größere Toleranzen als andere Betriebsarten. Dadurch kann stärker verrundet werden, was zu geringeren Beschleunigungen führt. Dies ermöglicht im Mittel größere Vorschübe&lt;/li&gt;&#10;&lt;li&gt;Die zu erwartenden Fräskräfte sind geringer: Keine Durchfräsungen, kaum Energieeinsatz bei der Spanverdrängung, kleineres Spanvolumen als bei Schaftfräsern&lt;/li&gt;&#10;&lt;li&gt;Dadurch kann die Maschine näher an ihren mechanischen und elektrischen Grenzen betrieben werden&lt;/li&gt;&#10;&lt;li&gt;Durch im Schnitt größere G-Code Dateien mehr Optimierungspotential&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="schritt-1-besserer-g-code"&gt;Schritt 1: Besserer G-Code&lt;/h2&gt;&#10;&lt;p&gt;Dieser Abschnitt folgt dem Credo &lt;code&gt;Garbage in, Garbage out&lt;/code&gt;. Will heißen: Wenn mein G-Code schon schlecht ist, kann die Maschine niemals optimale Fertigungszeiten erreichen. Was zeichnet also &amp;ldquo;schlechten&amp;rdquo; G-Code in Bezug auf Fertigungszeit aus?&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;übermäßig komplex und lang, möglicherweise Dateigröße im Megabyte-Bereich&lt;/li&gt;&#10;&lt;li&gt;extrem viele Stützpunkte und eine Menge kurzer Liniensegmente&lt;/li&gt;&#10;&lt;li&gt;Einen Haufen &amp;ldquo;Leerfahrten&amp;rdquo; im Eilgang auf XY mit hochgezogener Z-Achse&lt;/li&gt;&#10;&lt;li&gt;Viele Rückzüge der Z-Achse auf Sicherheitshöhe&lt;/li&gt;&#10;&lt;li&gt;Häufige Wartezeiten der Maschine (Spindelhochlauf, Kühlmittelpumpe, Controller-Puffer voll)&lt;/li&gt;&#10;&lt;li&gt;Übermäßiges Feinstschlichten auch bei B-Oberflächen&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="aber-wie-soll-ich-meinen-g-code-vereinfachen"&gt;Aber wie soll ich meinen G-Code vereinfachen?&lt;/h3&gt;&#10;&lt;p&gt;Kurze Frage, lange Antwort: Es fängt im CAD an und hört möglicherweise im CAM nicht auf.&lt;/p&gt;&#10;&lt;h3 id="beispiel-cad"&gt;Beispiel CAD&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-20-bline.jpg" alt="Image: Letter &amp;#39;B&amp;#39; in CAM, drawn as a set of line segments" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Nehmen wir an, wir möchten eine Schrift fräsen. Unser CAD-Programm hat auch einen Haufen toller Schriftarten dazu. Wir erzeugen nun eine DXF-Datei für&amp;rsquo;s CAM. Im CAM dann sehen wir plötzlich beim Hineinzoomen, dass alle Buchstaben aus sehr kurzen Liniensegmenten bestehen statt aus Kreissegmenten oder &lt;a href="https://en.wikipedia.org/wiki/Spline_%28mathematics%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Splines&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Die Folge: Das CAM wird &lt;code&gt;G01&lt;/code&gt;-Bewegungen, also &amp;ldquo;Linear Moves&amp;rdquo; bzw. eine Aneinanderreihung von Strecken erzeugen.&lt;/p&gt;&#10;&lt;p&gt;Die verbundenen Strecken machen sehr viele Richtungswechsel der Maschine erforderlich. Folglich muss sie vor dem Verbindungspunkt ständig abbremsen und danach wieder beschleunigen. Die Maschine kann also nicht mit konstanter Geschwindigkeit fahren, denn das würde eine unendliche Beschleunigung am Knickpunkt erfordern.&lt;/p&gt;&#10;&lt;p&gt;Für dieses Problem gibt es Lösungen, die im Idealfall gemeinsam angewendet werden:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Im CAD-Programm einstellen, dass wann immer möglich Splines oder Kurven statt Liniensegmenten erstellt werden sollen&lt;/li&gt;&#10;&lt;li&gt;Vereinfachung der Zeichnung durch Reduktion der Stützstellen&lt;/li&gt;&#10;&lt;li&gt;Vermehrte Nutzung von Radien und Splines an Kanten, Vermeidung von Winkeln&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;In meinem Falle war die erste Option nicht verfügbar, sodass ich nun ein anderes Programm für die Gravur von Schriften verwende als für meine üblichen Zeichnungen. Dadurch kann ich Schrift jetzt in Linien- und Bogensegmenten ausdrücken. Die dafür erforderliche Fräszeit halbiert sich und gleichzeitig kommt der G-Code mit weniger Zeilen aus. Außerdem wird die Schrift runder und hält sich sogar noch genauer an die anfangs gewählte Schriftart.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-20-bsplinevsline.jpg" alt="Image: Letter &amp;#39;B&amp;#39; in CAM, twice. Once per spline, once as line segments" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Gute Beispiele für die Vereinfachung von Pfaden durch Reduktion von Stützpunkten liefern Gimp und Inkscape - für Inkscape habe ich das sogar bereits meinem &lt;a href="https://blog.schallbert.de/path-from-image/#inkscape"&gt;Artikel zum Thema Bild-zu-Pfad-Konversion&lt;/a&gt; gezeigt. Mit ein paar wenigen Klicks erhält man auf Kosten der Genauigkeit Zeichnungsdaten, die teils deutlich schneller zu fertigen sind.&lt;/p&gt;&#10;&lt;p&gt;Da ein Schaftfräser sowieso keine Tasche mit &lt;code&gt;r=0&lt;/code&gt; in den Ecken fertigen kann, habe ich mir angewöhnt, sämtliche Ecken und Kanten im CAD abzurunden. Nicht nur liegen so hergestellte Teile besser in der Hand, sie sind außerdem schneller zu fertigen - die Zeitersparnis summiert sich, besonders auf langsamen und trägen Maschinen.&lt;/p&gt;&#10;&lt;h3 id="beispiel-cam"&gt;Beispiel CAM&lt;/h3&gt;&#10;&lt;p&gt;Im CAM kann ich durch vermehrte Verwendung von Anfahrpunkten und Rampen ein wenig Zeit sparen. Ich lege sie zumeist an das kürzeste Liniensegment und spare so einen Brems/Beschleunigungsvorgang. Bei der Eintauchart ist der Vorteil von &amp;ldquo;Rampe&amp;rdquo; gegenüber &amp;ldquo;Helix&amp;rdquo;, dass sämtliche Wege des Fräsers Nutzwege sind und ich ohne zusätzliche Achsbeschleunigungen durchfahren kann. Je nach Rampensteigung allerdings verlängert sich der Fräspfad, da Überlappungen entstehen.&lt;/p&gt;&#10;&lt;p&gt;Einen viel größeren Effekt auf die Fertigungszeit hat die kluge Auswahl von Räumzustellungen und -strategien für Taschen passend zu Material, Fräser und gewünschter Oberflächengüte. So erzeugt Wirbelfräsen (Trochoidalfräsen&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;) in vielen Fällen längere Dateien und kostet auf der Maschine mehr Zeit als der klassische &amp;ldquo;Rasenmäher&amp;rdquo;, welcher die Tasche bloß abzeilt. Für die Räumzustellung wähle ich bei Materialien wie Holz und Acrylglas gern große Werte über &lt;code&gt;60%&lt;/code&gt; - es sei denn, der Boden der Tasche muss besonders hohen Qualitätsansprüchen genügen.&lt;/p&gt;&#10;&lt;p&gt;Für Gravuren ist es meiner Meinung nach oft am Schnellsten, auf einen Räumgang mit einem zweiten Fräser zu verzichten und wenn möglich einen Gravurfräser mit abgeflachter Spitze - ähnlich derer für meine &lt;a href="https://blog.schallbert.de/cut-dibond-tests/"&gt;Tests mit Dibond&lt;/a&gt; - zu verwenden. Dieser benötigt zum Räumen zwar länger; das kann ich durch gesteigerte Gravurtiefe und eine recht breite Abflachung von &lt;code&gt;1.5mm&lt;/code&gt; aber kompensieren. Ich verwende bei einem solchen Fräser eine Räumzustellung in XY von &lt;code&gt;1mm&lt;/code&gt;. Übrig bleibt schließlich der Zeitvorteil durch weniger Werkzeugwechsel.&lt;/p&gt;&#10;&lt;h3 id="aber-wie-soll-ich-die-leerfahrten-reduzieren"&gt;Aber wie soll ich die Leerfahrten reduzieren?&lt;/h3&gt;&#10;&lt;p&gt;Endlich mal ein paar knackig-kurze Antworten:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Geringere Rückzugshöhe in &lt;code&gt;Z&lt;/code&gt; (ich verwende &lt;code&gt;5mm&lt;/code&gt; aber &lt;code&gt;2mm&lt;/code&gt; würde bei mir wahrscheinlich auch funktionieren)&lt;/li&gt;&#10;&lt;li&gt;Optimierung der Frässtrategie im CAM auf &amp;ldquo;minimale Wege&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Bei Nesting: Darauf achten, dass die Geometrie möglichst nicht Zeile für Zeile, sondern Teil für Teil gefertigt wird.&lt;/li&gt;&#10;&lt;li&gt;Im Zweifel Optimierung der Strategie im G-Code von Hand. Lohnt sich dann, wenn das CAM-Programm nicht gut genug arbeiten kann und viele Gleichteile zu fertigen sind&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="und-wo-ist-das-problem-mit-rückzügen-der-z-achse"&gt;Und wo ist das Problem mit Rückzügen der Z-Achse?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Rückzüge finden vor allem im Gravurbetrieb sehr häufig statt&lt;/li&gt;&#10;&lt;li&gt;der Zeitverlust summiert sich&lt;/li&gt;&#10;&lt;li&gt;Beim Wiedereingriff wird meist nicht im Eilgang sondern mit für den Fräser vorgesehener Geschwindigkeit mit &lt;code&gt;G01&lt;/code&gt; auf das Material gefahren&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Eine Verbesserung kann hier eine gute Planung der Fräsreihenfolge im CAM bringen. Möglichst viele Sektionen sollten verbunden und ohne Absetzen des Fräsers gefertigt werden. Im CAM sollte für Rückzug und Wiedereinfahrt in &lt;code&gt;Z&lt;/code&gt; der Eilgang verwendet werden.&lt;/p&gt;&#10;&lt;h3 id="wie-reduziere-ich-wartezeiten"&gt;Wie reduziere ich Wartezeiten?&lt;/h3&gt;&#10;&lt;p&gt;Mit Kühlschmierstoffschaltvorgängen, Wechsel der Spindeldrehzahl oder des Werkzeugs sind Wartezeiten verbunden, die meist leicht zu reduzieren sind:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Kühlschmierstoffförderung durchlaufen lassen während des Programmablaufes&lt;/li&gt;&#10;&lt;li&gt;die Verwendung möglichst weniger Werkzeuge bei gleichbleibender Drehzahl für ein Produkt&lt;/li&gt;&#10;&lt;li&gt;Anschläge verwenden, um die Maschinenbeschickung zu vereinfachen und Nullpunkte nicht immer wieder neu setzen zu müssen&lt;/li&gt;&#10;&lt;li&gt;Wird die Maschine mit komplett verschiedenen parametern für Gravur- und Fräsbetrieb gefahren, empfiehlt sich die doppelte Installation der Steuersoftware mit jeweils passenden Parametersätzen. Dadurch entfällt das ständige Ändern der Konfiguration.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Weiter geht es in &lt;a href="https://blog.schallbert.de/cnc-tuning-kinematics/"&gt;&lt;em&gt;Teil 2&lt;/em&gt;: Kinematische Parameter hochdrehen&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;Schrittverluste treten dann auf, wenn die Achse eines Schrittmotors nicht mehr synchron mit der durch die Steuerung vorgegebenen Schrittfolge dreht, sondern weniger bzw. mehr Schritte zurücklegt als vorgegeben. Fast immer ist die Ursache eines Schrittverlustes in einer unzureichenden Auslegung des Systems (z.B. zu geringe oder instabile Versorgungsspannung, zu schwache oder nicht an den Motor angepasste Endstufen) oder einer Überlastung des Schrittmotors zu finden. Selten jedoch können Schrittverluste Symptome von Defekten sein wie Lagerfraß, Windungsschluss oder durchschlagene Pufferkondensatoren.&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;Das Trochoidalfräsen ist keine schlechte Praxis. Es hat andere Stärken, ist im Gravurrbetrieb aber nur selten von Nutzen.&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>'redir' im Caddyserver</title><link>https://blog.schallbert.de/migrating-to-subdomain/</link><pubDate>Tue, 30 Jan 2024 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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 00:00:00 +0000</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;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 00:00:00 +0000</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;h3 id="update-aug-2024"&gt;Update Aug-2024&lt;/h3&gt;&#10;&lt;p&gt;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 vorgeschlagene Lösung ganz charmant:&lt;/p&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;h3 id="update-jul-2025"&gt;Update Jul-2025&lt;/h3&gt;&#10;&lt;p&gt;Ich habe jetzt eine Lösung für &lt;em&gt;Gitea&lt;/em&gt;, aber auch alle anderen meiner Websites zustande gebracht:&lt;/p&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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&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 00:00:00 +0000</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;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;h2 id="update-nov-2025"&gt;Update Nov-2025&lt;/h2&gt;&#10;&lt;p&gt;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;&lt;/p&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;&#10;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;h3 id="update-jan-2024"&gt;Update Jan-2024&lt;/h3&gt;&#10;&lt;p&gt;Es es gibt zwei weitere Möglichkeiten dafür, dass der &lt;code&gt;toolcache&lt;/code&gt; nicht gefunden wird:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Ich habe das Volume nicht der Action, sondern dem Runner zur Verfügung gestellt. Ich bin mir nicht sicher, dass die Daten auch in der Action verfügbar sind.&lt;/li&gt;&#10;&lt;li&gt;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 automatisch passiert. Dennoch bin ich mir nicht 100% sicher, ob die Information noch dem Stand der Dinge entspricht.&lt;/li&gt;&#10;&lt;/ol&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 00:00:00 +0000</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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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;p&gt;&lt;em&gt;Update&lt;/em&gt;: Alternativ kann man der Überschrift auch eine sprachunabhängige &amp;ldquo;ID&amp;rdquo; 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;.&lt;/p&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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;iframe src="https://makertube.net/videos/embed/jFD7nxftVHjUy6LKPCFE3N" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;QR-codengrave Quick Demo&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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>Estlcam: Inverse Gravuren</title><link>https://blog.schallbert.de/negative-carving-with-estlcam/</link><pubDate>Tue, 27 Sep 2022 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/negative-carving-with-estlcam/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_carve_results-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: A negative carve in wood"&#10; title="Estlcam: Inverse Gravuren" /&gt;&#10;&lt;p&gt;Der Plan war, ein Holzschild mit meiner CNC zu fertigen. Es sollte jedoch invertiert sein. Das heißt, die Lettern sollten sich vom flachen Hintergrund abheben. Da es verschiedene Wege gibt, so etwas in Estlcam umzusetzen (ein sogenanntes Computer Aided Machining Programm, was aus Zeichnungsdaten Fräspfade und später Maschineninstruktionen erzeugen kann), aber nicht alle Wege leicht zu finden oder gut anwendbar sind, biete ich hier eine Schritt-für-Schritt Anleitung an.&lt;/p&gt;&#10;&lt;h3 id="1-den-rahmen-des-schildes-importieren"&gt;1. Den Rahmen des Schildes importieren&lt;/h3&gt;&#10;&lt;p&gt;Zuerst sollten Zeichnungsdaten des Rahmens von dem zu fertigenden Schild vorliegen, beispielsweise im &lt;code&gt;DXF&lt;/code&gt;-Format. Ich habe ein rechteckiges Schild mit abgerundeten Ecken und definierter Rahmenstärke in meinem CAD-Programm gezeichnet. Es gilt sicherzustellen, dass die Frästiefe später nicht die Breite des Rahmens überschreitet, ansonsten könnte die Fräsarbeit zu instabil werden.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_frame.jpg" alt="Image: Frame for my sign" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="2-den-text-hinzufügen"&gt;2. Den Text hinzufügen&lt;/h3&gt;&#10;&lt;p&gt;Jetzt kann der Inhalt des Schildes in Form von Text durch Tastendruck auf das &amp;ldquo;Carve&amp;rdquo;-Symbol hinzugefügt werden. Dabei ist es sinnvoll, die Breite der Textbox an die inneren Rahmengrenze anzupassen, sodass der Text stets hineinpasst. Man hat verschiedene Carve-Stile zur Auswahl. Für die hier dargestellte Fräsarbeit ist &lt;code&gt;Carve aussen&lt;/code&gt; zu wählen. Nun den Text eintippen, Schriftart selektieren und die Schrifthöhe so anpassen, dass es im Schild ein harmonisches Bild ergibt.&lt;/p&gt;&#10;&lt;p&gt;Abhängig vom zu verwendenden Fräser sollte dann die Frästiefe gewählt werden. Bei kleineren Winkeln der V-Nutfräsers ist die Frästiefe meist größer zu wählen als bei breiten 90°-Fräsern wie ich ihn verwende. Für mein &lt;code&gt;12mm&lt;/code&gt; starkes Plattenmaterial setze ich eine Frästiefe von &lt;code&gt;4.5mm&lt;/code&gt; an. Anschließend wird die maximale Fräsbreite festgelegt. Gemeint ist hier, wie breit die &amp;ldquo;Täler&amp;rdquo; zwischen den Buchstaben sein sollen. Da ich für den invertierten Carve eine &amp;ldquo;Talsohle&amp;rdquo; benötige, habe ich hier einen sehr großen Wert eingestellt. Zum Schluss dann sollte die Taste &lt;code&gt;Umrahmung auswählen&lt;/code&gt; geklickt werden. Hiermit ist die Fräsarbeit dann nach außen begrenzt.&lt;/p&gt;&#10;&lt;p&gt;Bitte dieses Bearbeitungsfenster vorerst geöffnet lassen.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_carve_text.jpg" alt="Image: Settings for the text to carve" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="3-werkzeugauswahl"&gt;3. Werkzeugauswahl&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_carve_preview1.jpg" alt="Image: Carve preview 1" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Wählen Sie nun das V-Nutwerkzeug durch Klicken in der entsprechenden Zeile Ihrer Werkzeugliste für die Textfräsung aus. Stellen Sie sicher, dass das Häkchen bei &lt;code&gt;Textobjekt zerlegen&lt;/code&gt; im Fenster &amp;ldquo;Text Fräsen&amp;rdquo; nicht erscheint, denn nur dann können Sie weitere Anpassungen vornehmen. Schließen Sie nun dieses Fenster per Klick auf &lt;code&gt;OK&lt;/code&gt;. Nun wählen Sie &lt;code&gt;Vorschau&lt;/code&gt; aus, um sich einen Eindruck von der Fräsarbeit zu verschaffen. Sehen Sie die eng beisammenstehenden parallelen Linien rechts im Bild? Dies wird durch den Parameter &lt;code&gt;Räumzustellung&lt;/code&gt; aus der Werkzeugtabelle verursacht. Für saubere Arbeiten mit einem Gravurfräser sollte dieser Wert bei kleiner gleich &lt;code&gt;5%&lt;/code&gt; bleiben.&lt;/p&gt;&#10;&lt;p&gt;Hmm, so werden Sie noch keine befriedigenden Arbeitsergebnisse erhalten, und die Fräsarbeit wird lange dauern. Wir benötigen also zusätzlich ein paar Räumpfade mit einem normalen Schaftfräser.&lt;/p&gt;&#10;&lt;h3 id="4-räumen"&gt;4. Räumen&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_carve_clearing_tool.jpg" alt="Image: Carve preview 1" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Speichern Sie Ihr Projekt spätestens jetzt ab. Nach dem aktuellen Schritt kann der Text nicht mehr bearbeitet werden!&lt;/p&gt;&#10;&lt;p&gt;Um den Fräser für die Räumung der &amp;ldquo;Talsohle&amp;rdquo; hinzuzufügen, wählen Sie den Carve-Pfad aus und wählen &lt;code&gt;Textobjekt zerlegen&lt;/code&gt; an. Bestätigen Sie mit &lt;code&gt;OK&lt;/code&gt; und wählen den Fräspfad erneut aus. Nun erscheint die Werkzeugkonfiguration. Fügen Sie das Räumwerkzeug hinzu. Vorzugsweise ist sein Durchmesser geringer als der Abstand zwischen Schrift und Rahmen, sodass eine möglichst große Fläche später glatt geräumt werden kann (Anmerkung: Ich habe das nicht gemacht, weswegen auf meinem Beispiel ein paar raue Stellen übrig geblieben sind). Die Räumzustellung dieses Fräsers wähle ich bei Holz zwischen 20% und 40%.&lt;/p&gt;&#10;&lt;p&gt;Wenn Sie für die Gravur einen Fräser ohne Abflachung am Boden gewählt haben (ich empfehle hier gern &lt;code&gt;0.5mm&lt;/code&gt; als guten Kompromiss zwischen hoher Auflösung und guter Fertigungsqualität), können Sie die Räumzustellung des Fräsers in der Werkzeugtabelle zur Verbesserung der Oberflächenqualität noch einmal nach unten korrigieren.&lt;/p&gt;&#10;&lt;h3 id="5-zweite-vorschau"&gt;5. Zweite Vorschau&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_carve_preview2.jpg" alt="Image: Carve preview 2" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ok, jetzt sollten wir durch sein. Nochmal kurz die Vorschau ausgewählt und sich über die reduzierte Verarbeitungszeit gefreut, und ran an die Maschine?&lt;/p&gt;&#10;&lt;p&gt;Ah, eine weitere Optimierung kann für das Gravurfräsen angebracht sein: Unter &lt;code&gt;Einstellungen&lt;/code&gt; die Sicherheitshöhe auf ein Minimum reduzieren (aber ohne in Klemmen oder andere Hindernisse zu rauschen). Das ständige Aus- und Eintauchen ins Material kostet bei Gravuren in Estlcam immer recht viel Zeit, wenn sich die Sicherheitshöhe in großem Abstand zur Werkstückoberfläche befindet.&lt;/p&gt;&#10;&lt;p&gt;Keine Sorge, wenn die Schrift in der Vorschau nun nicht mehr lesbar ist. Das ist total normal, weil ja nur die (jetzt wegen Verwendung des Räumfräsers gröber dargestellten) Fräspfade angezeigt werden.&lt;/p&gt;&#10;&lt;p&gt;Nach einer letzten Prüfung von Vorschüben und Werkzeugen kann&amp;rsquo;s jetzt endlich losgehen :)&lt;/p&gt;&#10;&lt;h3 id="engrave"&gt;6. Auf die CNC damit!&lt;/h3&gt;&#10;&lt;p&gt;Viel Spaß beim Fräsen. Ich für meinen Teil fand es sehr entspannend, der Maschine dabei zuzusehen, wie sie langsam ein immer deutlicher werdendes Bild ins Holz schneidet, was aber erst bei Verwendung des V-Fräsers lesbar wird.&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_carve_results.jpg" alt="Image: The completed sign" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;</description></item><item><title>Favicon und Sammlungsgenerator</title><link>https://blog.schallbert.de/jekyll-icon-paginator/</link><pubDate>Sun, 13 Jun 2021 00:00:00 +0000</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>Jekyll: Bilder</title><link>https://blog.schallbert.de/jekyll-images/</link><pubDate>Mon, 07 Jun 2021 00:00:00 +0000</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 00:00:00 +0000</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;&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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 00:00:00 +0000</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>🎄 Steckbrett-Lagerfeuer</title><link>https://blog.schallbert.de/projects/fireplace/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/fireplace/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/fireplace/fireplace-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: LED-Fireplace, Arduino-controlled"&#10; title="🎄 Steckbrett-Lagerfeuer" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeit: Anfänger 1/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: 8&amp;hellip;25€&lt;/li&gt;&#10;&lt;li&gt;Zeit ~1h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="überblick"&gt;Überblick&lt;/h2&gt;&#10;&lt;p&gt;Dies ist ein schnelles und einfaches Projekt, das zeigt, wie man mit einem Microcontrollerboard und einigen LEDs eine warme und gemütliche Wohnzimmeratmosphäre schaffen kann. An eine Powerbank angeschlossen, wird diese kleine Bastelei den ganzen Abend leuchten und eine warme und gemütliche Atmosphäre schaffen. Ich habe Teile der Idee für dieses Projekt dem Buch &amp;ldquo;Tiny AVR for the evil Genius&amp;rdquo; entliehen, in dem ich viel gelesen habe, als ich vor Jahren mit dem Entwurf eingebetteter Software in &lt;code&gt;C&lt;/code&gt; begann.&lt;/p&gt;&#10;&lt;p&gt;Die Projektkosten hängen von den gewählten Komponenten ab. Wenn ein original Arduino verwendet wird, liegen sie am oberen Ende des Bereichs. So kann man den hart arbeitenden Entwicklern und Ingenieuren von &lt;a href="https://www.arduino.cc/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Arduino&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Anerkennung zollen, die Open-Source-Lösungen für uns alle schaffen 🥳.&lt;/p&gt;&#10;&lt;h2 id="die-hardware"&gt;Die Hardware&lt;/h2&gt;&#10;&lt;p&gt;Natürlich kann man jedes Mikrocontroller-Board verwenden, das mindestens 8 digitale Ausgänge und 1 analogen Eingang unterstützt. In meinem Beispiel verwende ich ein &lt;a href="https://www.arduino.cc/hardware#nano-family" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Arduino Nano&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (Nachbau), das ich irgendwo herumfliegen hatte. Zeit für ein Upgrade auf das &amp;ldquo;Original&amp;rdquo;&amp;hellip;&#10;Des Weiteren benötigt man ein Steckbrett, ein paar Drähte und acht LEDs. Ich empfehle, 5 warmweiße LEDs, 2 gelbe LEDs und 1 rote LED mit ähnlicher Helligkeit (Lumenzahl) zu verwenden, um Farben zu erhalten, die einem echten Feuer ähnlich sind.&lt;/p&gt;&#10;&lt;h2 id="die-software"&gt;Die Software&lt;/h2&gt;&#10;&lt;p&gt;Die Software erzeugt einen flackernden Lichteffekt mit unterschiedlicher Geschwindigkeit.&lt;/p&gt;&#10;&lt;h3 id="einrichtung"&gt;Einrichtung&lt;/h3&gt;&#10;&lt;p&gt;Beim Start definiere ich in &lt;code&gt;setup()&lt;/code&gt; die Pins, an die die LEDs angeschlossen sind, als Ausgänge.&#10;Ich lasse einen offenen Analog-Pin einmal auslesen und nehme dessen (undefinierten) Wert als Seed für den später besprochenen Pseudo-Zufallszahlengenerator.&lt;/p&gt;&#10;&lt;h3 id="schleife"&gt;Schleife&lt;/h3&gt;&#10;&lt;p&gt;In der Endlosschleife berechnet das Programm eine neue Zufallszahl und übergibt diese dann an die nachgeschalteten Funktionen. Hier erzeugt ein Zufallszahlengenerator einen Wert, der dann verwendet wird, um&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;zu bestimmen, welche der acht LEDs leuchten soll&lt;/li&gt;&#10;&lt;li&gt;die Zeit festzulegen, für die dieser LED-Status beibehalten werden soll.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Schließlich werden die LEDs entsprechend gesetzt und die Schleife wiederholt sich am Anfang.&lt;/p&gt;&#10;&lt;h3 id="linear-rückgekoppeltes-schieberegister"&gt;Linear rückgekoppeltes Schieberegister&lt;/h3&gt;&#10;&lt;p&gt;Der Zufallszahlengenerator ist als sogenanntes &lt;a href="https://en.wikipedia.org/wiki/Linear-feedback_shift_register" target="_blank" rel="noopener noreferrer" class="external-link"&gt;linear feedback shift register (LSFR)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; mit einer Länge von 32bit implementiert (wiederholt sein Muster nach max. 2^32 Schritten). Diese Implementierung verwendet den &amp;ldquo;Galois-Typ&amp;rdquo; von LFSRs.&lt;/p&gt;&#10;&lt;h3 id="galois-lfsr-implementierung"&gt;Galois LFSR Implementierung&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-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;loop&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; iRandNum &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (iRandNum &lt;span style="color:#f92672"&gt;&amp;gt;&amp;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:#f92672"&gt;-&lt;/span&gt;(iRandNum &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1u&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0xd0000001u&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;In dieser Codezeile passiert eine Menge, also schauen wir sie uns mal genauer an.&lt;/p&gt;&#10;&lt;p&gt;Wesentlich für diese Art von Zufallszahlengeneratoren ist die &lt;a href="https://en.wikipedia.org/wiki/XOR_gate" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;XOR&lt;/code&gt;-Operation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die durch ein &lt;code&gt;^&lt;/code&gt; in der Mitte der Codezeile angezeigt wird. XOR&amp;quot; bedeutet &amp;ldquo;eXclusive OR&amp;rdquo;, das nur dann eine logische 1 ausgibt, wenn die logischen Eingänge A und B &lt;em&gt;unterschiedlich&lt;/em&gt; sind. Wenn &amp;ldquo;A&amp;rdquo; und &amp;ldquo;B&amp;rdquo; beide &amp;ldquo;0&amp;rdquo; oder beide &amp;ldquo;1&amp;rdquo; sind, wird dieses Gatter &amp;ldquo;0&amp;rdquo; ausgeben.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Beispiel für binäres XOR:&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; A B Ergebnis&#10;0b00001111 ^ 0b00110011 = 00111100&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Links vom &lt;code&gt;XOR&lt;/code&gt;-Operator wird die aktuelle Zufallszahl um &lt;code&gt;1&lt;/code&gt; nach rechts verschoben (&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;). Dabei wird ihr gesamter Inhalt wird um ein Bit geschoben. In numerischer Hinsicht bedeutet dies, dass der Wert ganzzahlig durch zwei dividiert wird.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Beispiel für binäres SHIFT:&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; A B Ergebnis Als Zahl:&#10;0b00001111 &amp;gt;&amp;gt; 1 = 0b00000111 15 &amp;gt;&amp;gt; 1 = 7&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;OK, analysieren wir die obige Zeile Code weiter.&#10;Direkt neben dem &lt;code&gt;XOR&lt;/code&gt;-Operator findet eine weitere Operation statt: Ein logisches &lt;code&gt;AND&lt;/code&gt; (&lt;code&gt;&amp;amp;&lt;/code&gt;) zwischen unserer aktuellen Zufallszahl und &lt;code&gt;1u&lt;/code&gt;, was &amp;ldquo;unsigned 1&amp;rdquo; bedeutet. Dabei werden alle höheren Bits des Zufallszahlengenerators maskiert. Das Ergebnis dieser Aktion ist eine einfache &amp;ldquo;0&amp;rdquo;, wenn die Zahl eine &amp;ldquo;0&amp;rdquo; als niedrigstwertiges Bit hat usw.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Beispiel für binäres UND:&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; A B Ergebnis &#10;0b00001111 &amp;amp; 1 = 0b00000001&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Die Negation am Anfang dieses Terms bedeutet, dass, wenn das Ergebnis des vorherigen &lt;code&gt;&amp;amp;&lt;/code&gt; &lt;code&gt;0&lt;/code&gt; war, der gesamte Term wieder &lt;code&gt;0b00000000...&lt;/code&gt; sein wird. Wenn das Ergebnis &lt;code&gt;-1&lt;/code&gt; lautet, sieht der 2er-Komplement-Binärcode stattdessen wie folgt aus: &lt;code&gt;0b1111111...&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Dies ist eine einfache Möglichkeit, einen einzelnen booleschen Wert auf einen beliebigen längeren Datentyp anzuwenden, ohne eine Verzweigungslogik wie bei einer &lt;code&gt;if()&lt;/code&gt;-Klausel zu verwenden.&lt;/p&gt;&#10;&lt;p&gt;Und schließlich gibt es ein weiteres logisches &lt;code&gt;AND&lt;/code&gt; zwischen diesem Begriff und einer festen Bitmaske &lt;code&gt;0xd0000001u&lt;/code&gt;. Letztere sind die &amp;ldquo;Anzapfungen&amp;rdquo; des LFSR, d.h. welche Bits des aktuellen Wertes in das System zurückgeführt werden. Die Aufschlüsselung dieses Hex-Wertes in Boolesche Werte zeigt: 0xd = 0b1101&amp;quot;, d.h. die Bits 32, 31, 29 und 1 werden mit der aktuellen Zufallszahl maskiert, so dass nur ihre Werte unberührt bleiben, während alle anderen auf &amp;ldquo;0&amp;rdquo; gesetzt werden.&lt;/p&gt;&#10;&lt;h3 id="galois-lfsr-zusammenfassung"&gt;Galois LFSR Zusammenfassung&lt;/h3&gt;&#10;&lt;p&gt;Puh, das war eine Menge. Es ist Zeit für ein paar Minuten Pause.&lt;/p&gt;&#10;&lt;p&gt;Gut, fassen wir die Codezeile oben nochmal zusammen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;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;iRandNum &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (iRandNum &lt;span style="color:#f92672"&gt;&amp;gt;&amp;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:#f92672"&gt;-&lt;/span&gt;(iRandNum &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1u&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0xd0000001u&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;Bedeutung: &amp;ldquo;Wenn das niedrigstwertige Bit der aktuellen Zufallszahl Null ist, dann verschiebe die Zufallszahl einfach nach rechts. Andernfalls XOR anwenden mit den abgegriffenen Bits der aktuellen Zufallszahl.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;LFSRs sind weit verbreitet in der &lt;a href="https://courses.cs.washington.edu/courses/cse466/12au/calendar/06-Modulation-posted.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;drahtlosen Kommunikationstechnik&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, sie sind interessant für &lt;a href="https://stackoverflow.com/questions/25415724/understanding-two-different-ways-of-implementing-crc-generation-with-lfsr" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Prüfsummenberechnungen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, [Kryptographen](https://medium. com/@czapfel/an-introduction-to-lfsrs-for-cryptography-bf2602640e91) und können sogar bei &lt;a href="https://evoniuk.github.io/posts/pitfall.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Programmierspielen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; nützlich sein, z.B. bei der Erstellung prozeduraler Karten. LFSRs sind einfach zu implementieren (wie man oben sieht, benötigen sie im Extremfall sogar nur eine einzige Codezeile!) und verbrauchen nicht viele Ressourcen. Hier ist ein hervorragender &lt;a href="https://datagenetics.com/blog/november12017/index.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Blogpost&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, der sich eingehend mit LFSRs beschäftigt, wenn Sie eher der visuelle Typ sind (und weniger der logische).&#10;Nachteilig ist, dass ihre &amp;ldquo;Zufallsqualität&amp;rdquo; schlecht ist, weil sie deterministisch sind.&lt;/p&gt;&#10;&lt;p&gt;Alles in allem fand ich das Thema so interessant, dass ich mich entschlossen habe, hier einen LFSR manuell zu implementieren, anstatt die Arduino-Funktion &lt;code&gt;random()&lt;/code&gt; zu verwenden.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://www.c64-wiki.de/images/9/9d/Pitfall_Animation2.gif" alt="Gif: Pitfall! Game scene" /&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Pitfall! Spielszene: prozedurales Leveldesign mittels LFSR&lt;/span&gt;&lt;a href="https://www.c64-wiki.de/wiki/Datei:Pitfall_Animation2.gif#file" class="attr-link" aria-label="Attribution 1"&gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="ein-problem-bleibt"&gt;Ein Problem bleibt&lt;/h3&gt;&#10;&lt;p&gt;Nun, wenn Sie die Codezeile vollständig verstanden haben, sehen Sie, was passiert, wenn die Zufallszahl nur Nullen enthält.&lt;/p&gt;&#10;&lt;p&gt;Er wird nie wieder aus diesem Zustand herauskommen 💀.&lt;/p&gt;&#10;&lt;p&gt;Deshalb sollte man diese Art von LFSR niemals mit &amp;ldquo;0&amp;rdquo; initialisieren.&lt;/p&gt;&#10;&lt;h3 id="ansteuerung-der-leds"&gt;Ansteuerung der LEDs&lt;/h3&gt;&#10;&lt;p&gt;Der Rest der Software hat viel mehr Zeilen, macht aber auch viel mehr langweilige Sachen. Sie nimmt den generierten 32-Bit-Zufallswert und lässt ihn an den 8 LED-Ausgängen vorbeilaufen, d.h. jede LED &amp;ldquo;sieht&amp;rdquo; jedes Bit des Wertes. Zwischen jeder dieser Iterationen wird eine zufällige Verzögerung verwendet, so dass die Flackergeschwindigkeit auf eine Änderungsrate begrenzt ist, die wir Menschen noch wahrnehmen können.&lt;/p&gt;&#10;&lt;h2 id="die-hardware-1"&gt;Die Hardware&lt;/h2&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/fireplace/fireplace_detail.jpg" alt="Image: fireplace breadboard design detail view" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Zum Glück ist die Hardware einfacher zu erklären als die Software:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Ein Arduino Nano-Board ist mit einem Prototyping-Board verbunden.&lt;/li&gt;&#10;&lt;li&gt;Anoden der acht LEDs &lt;code&gt;(+)&lt;/code&gt; sind mit den digitalen Ausgängen &lt;code&gt;D2...D9&lt;/code&gt; verbunden.&lt;/li&gt;&#10;&lt;li&gt;Ihre Kathoden werden mit Jumpern gebrückt und mit dem &amp;ldquo;GND&amp;rdquo; des Arduino verbunden.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Dies ist die einfachste Konstruktion, die mir für diese Aufgabe einfiel.&lt;/p&gt;&#10;&lt;h2 id="verbesserungen"&gt;Verbesserungen&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Jede LED sollte einen Vorwiderstand bekommen.&#10;&lt;ul&gt;&#10;&lt;li&gt;In meinem Design ist der Strom nur durch den maximalen Strom begrenzt, den der Arduino Pin treiben kann.&lt;/li&gt;&#10;&lt;li&gt;Entweder kann die LED deutlich mehr als &lt;code&gt;40mA&lt;/code&gt; Spitzenstrom verkraften oder sie wird unweigerlich irgendwann durchbrennen.&lt;/li&gt;&#10;&lt;li&gt;Bei der Berechnung des Widerstands sollte man sich an das Datenblatt der LED halten, da verschiedene LED-Farben unterschiedliche Spannungen haben.&lt;/li&gt;&#10;&lt;li&gt;Beispiel für eine Widerstandsberechnung bei einer Versorgungsspannung von &lt;code&gt;5V&lt;/code&gt; für eine Warmweiße LED: &lt;code&gt;20mA @ 3.1V --&amp;gt; R = U/I = (Usup-Uled) / I = 1.9 / 0.02 = 95Ohm&lt;/code&gt;. Nächster Widerstand aus der E6-Reihe &lt;code&gt;100Ohm&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Jede LED könnte einen Parallelkondensator erhalten.&#10;&lt;ul&gt;&#10;&lt;li&gt;Dies würde den Flackereffekt glätten.&lt;/li&gt;&#10;&lt;li&gt;Man könnte Videos aufnehmen, in denen die Kamera nicht durch das Flackern der LEDs gestört wird. Das wäre doch schön.&lt;/li&gt;&#10;&lt;li&gt;Beispiel Kondensatorberechnung: Durchschnittlicher Strom im ausgeschalteten Zustand &lt;code&gt;20mA&lt;/code&gt;, erlaubter Spannungsabfall &lt;code&gt;0.6V&lt;/code&gt; innerhalb von &lt;code&gt;1ms&lt;/code&gt;. Vorgeschlagene Kapazität ist dann &lt;code&gt;20mA * 1ms / 0.6V = 33uF&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="zeit-für-eine-diy-replik"&gt;Zeit für eine DIY-Replik?&lt;/h2&gt;&#10;&lt;p&gt;Bitteschön!&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/led-fireplace-diy/"&gt;Steckbrett-Lagerfeuer DIY Anleitung&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 00:00:00 +0000</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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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 href="https://commons.wikimedia.org/wiki/File:Jekyll_%28software%29_Logo.png#file" class="attr-link" aria-label="Attribution 1"&gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/hero_landing.jpg" alt="Image: landing hero" /&gt;&lt;/figure&gt;&#10;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/hero_collection.jpg" alt="Image: collection hero" /&gt;&lt;/figure&gt;&#10;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;&#10;&lt;/div&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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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;p&gt;&lt;strong&gt;Update&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;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.&lt;/p&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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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;h3 id="update-jan-2024"&gt;Update Jan-2024&lt;/h3&gt;&#10;&lt;p&gt;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;&lt;/p&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;</description></item><item><title>GoogleTest Demo</title><link>https://blog.schallbert.de/projects/platformio_gtestgmock/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</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 zeitaufwändig 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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;&#10;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/mingw_install.jpg" alt="MinGW install" /&gt;&lt;/figure&gt;&#10;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;&#10;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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>PC-Upgrade</title><link>https://blog.schallbert.de/projects/casemodding-htpc/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/casemodding-htpc/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/htpc/htpc-upgrade-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Components for the PC upgrade"&#10; title="PC-Upgrade" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeit: Amateur 3/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: ~500€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~20h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="ziel-und-motivation"&gt;Ziel und Motivation&lt;/h2&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/htpc/htpc-before-upgrade.avif" alt="Image: My HTPC before the upgrade" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Im Jahr 2006 hatte ich mir einen Spiele-PC gegönnt. Als Gehäuse kaufte ich ein HTPC-Gehäuse. Es ist relativ flach und passt in das Rack einer Stereoanlage. 2015 tauschte ich ein erstes mal das Innenleben, als mir die Hardware zu langsam wurde. Ende 2025 war es wieder so weit: Das Netzteil zeigte sich mit der Leistungsaufnahme der neuen Grafikkarte überfordert, die Festplatten liefen voll und Spiele nicht mehr stabil.&lt;/p&gt;&#10;&lt;h2 id="umbauten"&gt;Umbauten&lt;/h2&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/htpc/htpc-after-upgrade.avif" alt="Image: Insides of the refreshed PC" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Kurz: Es muss neue Hardware her. Doch diese erzeugt viel mehr Abwärme als die Vorherige. Zur Optimierung des Luftstroms sind daher einige Umbauten vorzunehmen. Klingt doch nach einem tollen Projekt mit vielen Facetten, die dieses Blog ausmachen: Mechanik, Elektronik, Software, CNC-fräsen.&lt;/p&gt;&#10;&lt;h3 id="neue-komponenten"&gt;Neue Komponenten&lt;/h3&gt;&#10;&lt;p&gt;Einzubauen sind&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;650W Enermax Netzteil ersetzt 400W&lt;/li&gt;&#10;&lt;li&gt;Neues Motherboard MSI B550&lt;/li&gt;&#10;&lt;li&gt;AMD Ryzen5600x ersetzt Intel i5-6500&lt;/li&gt;&#10;&lt;li&gt;32GB DDR4 RAM statt 16GB&lt;/li&gt;&#10;&lt;li&gt;1TB PCIE SSD statt SATA SSDs&lt;/li&gt;&#10;&lt;li&gt;140mm Lüfter in 5.25&amp;quot; Schacht&lt;/li&gt;&#10;&lt;li&gt;2x60mm Lüfter in hintere Auslässe&lt;/li&gt;&#10;&lt;li&gt;96mm Lüfter für CPU, neuer Heatpipe-Kühler&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Die meisten Komponenten besorge ich mir gebraucht. Nur Netzteil, Platte und Lüfter sind Neuware. Doch schnell zeigt sich, dass ich das Gehäuse modifizieren muss: Die Grafikkarte kollidiert mit dem 3.5&amp;quot;-Schacht, wo bis dato Festplatten und Kartenlesegerät untergebracht waren. Der große 140mm-Lüfter passt nur an die Stelle, wo sich der 5.25&amp;quot;-Schacht befindet.&lt;/p&gt;&#10;&lt;p&gt;Also müssen beide Schächte entfernt oder mit dem Trennschleifer bearbeitet werden, um Platz zu schaffen. Außerdem benötige ich eine Montagevorrichtung für den Lüfter vor der 5.25&amp;quot;-Öffnung. Hingucker für das Gehäuse wäre es jetzt noch, die Laufwerkklappe automatisch steuern zu können. Dann würde sie nur bei laufendem Lüfter unter hoher Rechenlast geöffnet und im Normalbetrieb geschlossen bleiben.&lt;/p&gt;&#10;&lt;h3 id="anpassung-des-gehäusedeckels"&gt;Anpassung des Gehäusedeckels&lt;/h3&gt;&#10;&lt;p&gt;In meinem Artikel zum Thema &lt;a href="https://blog.schallbert.de/milling-secc/"&gt;Fräsen von SECC-Stahlblech&lt;/a&gt; zeige ich, wie man den Gehäusedeckel zwecks besserer Belüftung der Grafikkarte mit zusätzlichen Schlitzen ausstattet.&lt;/p&gt;&#10;&lt;h3 id="35-schacht"&gt;3.5&amp;quot;-Schacht&lt;/h3&gt;&#10;&lt;p&gt;{&amp;lt; image src=&amp;quot;/assets/images/htpc/floppy-bay-modified.avif&amp;quot; alt=&amp;ldquo;Image: Cutout in 3.5&amp;rdquo; bay&amp;quot; position=&amp;ldquo;right&amp;rdquo; &amp;gt;}&#10;Die Grafikkarte ist so lang, dass sie &lt;code&gt;1cm&lt;/code&gt; Überlappung mit dem 3.5&amp;quot;-Schacht besitzt. Zum Glück lässt sich der Schacht im Handumdrehen ausbauen. Also nehme ich den Trennschleifer zur Hand und bringe einen Ausschnitt in die Halterung ein. Anschließend bohre ich ein paar zusätzliche Löcher in die Seiten des Schachtes, sodass meine 2.5&amp;quot;-SSDs auf der von der Grafikkarte abgewandten Seite des Schachtes befestigt werden können. Auch der Schachtdeckel erhält ein weiteres Loch zur Befestigung am Gehäuse, da zwei Rastnasen durch den Ausschnitt für die Grafikkarte entfallen.&lt;/p&gt;&#10;&lt;p&gt;Der Kartenleser nutzt die volle Tiefe des Schachtes aus. Ich fürchte also schon, ihn gar nicht wieder montiert zu bekommen. Probehalber öffne ich sein Gehäuse und siehe da: Nur das vorderste Drittel wird tatsächlich von Leiterplatten belegt. Der hintere Teil ist anscheinend nur dafür da, Befestigungsmöglichkeiten zu bieten. Also trenne ich den hinteren Teil des Kartenlesergehäuses kurzerhand ab und baue alles wieder zusammen, sodass er sich nun passend im Schacht anbringen lässt.&lt;/p&gt;&#10;&lt;h3 id="525-schacht"&gt;5.25&amp;quot;-Schacht&lt;/h3&gt;&#10;&lt;p&gt;Der Schacht ist hoch genug, um zwei Laufwerke aufzunehmen. Ich benutze aber kaum noch CDs bzw. DVDs - und wenn, dann tut es auch ein externes Laufwerk. Den leeren Laufwerkkäfig kann ich also ausbauen und stattdessen an seiner Stelle einen Lüfter montieren. Dafür entwerfe ich im CAD eine Adapterplatte, die Befestigungsmöglichkeiten und Ausschnitte für den Lüfter enthält.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/htpc/drive-bay-mod.avif" alt="Image: mounting adapter to attach a case fan to the drive bay" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="lüfterplatzierung"&gt;Lüfterplatzierung&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/htpc/aux-fan.avif" alt="Image: External 140mm fan" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ich setze Gehäuselüfter gezielt ein, um zwei voneinander getrennte Luftströmungen im PC-Gehäuse zu erzeugen: Die CPU-Netzeil-Strömung verläuft an der Gehäusefront über einen 140mm-Lüfter an den CPU-Kühler, von wo aus die Warme Luft durch Netzteil und zwei weitere 60mm-Gehäuselüfter hinaus geleitet werden.&lt;/p&gt;&#10;&lt;p&gt;Der Luftstrom zur Grafikkarte wird über einen außerhalb des Gehäuses verbauten 140mm-Lüfter gestützt. Die Grafikkarte selbst fördert die erwärmte Luft direkt über den Gehäusedeckel nach draußen.&#10;Um den externen Lüfter anzuschließen, schneide ich ein Loch in eine Slotblende für PCI-Einbauschächte. Dort hinein passt genau die Buchse eines Lüftersteckers. Mittels Heißkleber sichere ich die Buchse in der Blende und schließe sie sonst den Lüfter auch an einen AUX-FAN Anschluss an.&lt;/p&gt;&#10;&lt;h2 id="klappensteuerung"&gt;Klappensteuerung&lt;/h2&gt;&#10;&lt;p&gt;Nun soll die Frontklappe noch automatisch öffnen, sobald der Lüfter dreht. Ist zwar komplizierter als wenn ich das von Hand mache, aber dieses Gimmick macht das Gehäuse doch erst richtig interessant. Ich entferne also das Klappenschloss und füge der Adapterplatte eine Halterung für ein Modellbauservo hinzu, welches die Klappe öffenn und schließen können soll.&lt;/p&gt;&#10;&lt;h3 id="elektronik"&gt;Elektronik&lt;/h3&gt;&#10;&lt;p&gt;Das Servo benötigt eine Ansteuerung. Außerdem möchte ich die Klappenzustand wie den Lüfter auch temperaturabhängig regeln, sodass ich die an dem Lüfter angelegte Spannung einlesen muss. Hierfür setze ich den Microcontroller &lt;a href="https://www.microchip.com/en-us/product/ATTINY44" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ATTINY44&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ein. Die Schaltung habe ich nur mit den allernötigsten Komponenten bestückt:&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/htpc/servo-control-pcb.avif" alt="Iamge: servo control circuit board" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Der Microcontroller gibt ein pulsweitengesteuertes Signal an den Servo aus. Dessen Spannungsversorgung wird über einen Elko gepuffert. Die Energieversorgung für den Gehäuselüfter leite ich über zwei Steckerleisten weiter. Dort greife ich das Signal mittels &lt;a href="https://de.wikipedia.org/wiki/Spannungsteiler" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Spannungsteiler&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ab und führe es einem Analogeingang zu. Ansonsten ist nur ein weiterer Widerstand verbaut, der den RESET-Eingang des Microcontrollers auf &lt;code&gt;VCC&lt;/code&gt; zieht und ihn so in Betrieb hält.&lt;/p&gt;&#10;&lt;h3 id="software-anforderungen"&gt;Software-Anforderungen&lt;/h3&gt;&#10;&lt;p&gt;Die Anforderungen an die Software sind einfach und klar:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Wenn der Lüfter steht, soll die Klappe schließen&lt;/li&gt;&#10;&lt;li&gt;Ist der Lüfter in Betrieb, soll die Klappe öffnen&lt;/li&gt;&#10;&lt;li&gt;Damit die Klappe nicht zu oft hin- und her fährt, soll der Klappenzustand mit etwas Verzögerung geändert werden und auch nur dann, wenn der Lüfter in der Zwischenzeit seinen Zustand beibehalten hat.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Die daraus entstandene Software ist auf &lt;a href="https://github.com/Schallbert/ServoKlappe/tree/main" target="_blank" rel="noopener noreferrer" class="external-link"&gt;meinem Github-Account&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; frei verfügbar.&lt;/p&gt;&#10;&lt;h3 id="software-hauptschleife"&gt;Software: Hauptschleife&lt;/h3&gt;&#10;&lt;p&gt;Bei der Ansteuerung des Modellbauservos mache ich es mir einfach: Ich verwende die &lt;a href="https://docs.arduino.cc/libraries/servo/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Servo-Bibliothek&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; von Arduino.&lt;/p&gt;&#10;&lt;p&gt;Das Programm macht nicht viel. Ich initialisiere das Servo &lt;code&gt;servoHatch&lt;/code&gt;, welches später im Programm angesteuert wird, definiere ein paar wenige Variablen und lege fest, auf welchem Pin der Spannungsteiler vom Lüfter liegt. Die Hauptschleife begnügt sich damit, die Spannung am Lüfter einzulesen und basierend darauf zu entscheiden, ob sich die Klappe bewegen sollte.&lt;/p&gt;&#10;&lt;p&gt;Die Auswertung übergebe ich dann der Klappensteuerung. Anschließend wartet das Programm &lt;code&gt;15ms&lt;/code&gt; und läuft dann erneut los.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;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;Servo servoHatch; &lt;span style="color:#75715e"&gt;// create Servo object to control a servo&#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;uint16_t&lt;/span&gt; fanVoltage{&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;}; &lt;span style="color:#75715e"&gt;// variable to read the value from the analog pin&#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;bool&lt;/span&gt; open{false};&#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; &lt;span style="color:#a6e22e"&gt;setup&lt;/span&gt;() {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pinMode(pinFanVoltage, 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;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; &lt;span style="color:#a6e22e"&gt;loop&lt;/span&gt;() {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fanVoltage &lt;span style="color:#f92672"&gt;=&lt;/span&gt; analogRead(pinFanVoltage); &lt;span style="color:#75715e"&gt;// reads the value of the fan voltage (value between 0 and 1023)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; open &lt;span style="color:#f92672"&gt;=&lt;/span&gt; mapHatchState(fanVoltage);&#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; setHatch(open);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; delay(&lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;); &lt;span style="color:#75715e"&gt;// waits for the servo to get there&#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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="software-hysterese"&gt;Software: Hysterese&lt;/h3&gt;&#10;&lt;p&gt;Mit der Funktion &lt;code&gt;mapHatchState&lt;/code&gt; lege ich den Sollzustand der Klappe in Abhängigkeit der Spannung am Lüfter fest.&lt;/p&gt;&#10;&lt;p&gt;Liegt die Spannung lange genug unter etwa &lt;code&gt;4.8V&lt;/code&gt;, so schließt die Klappe oder behält ihren geschlossenen Zustand bei. Liegt sie darüber, soll die Klappe geöffnet werden. Der Dauer des Zustandswechsels hängt von der Spannung selbst ab; bei Werten weit ab vom Schwellwert läuft der Aggregator schneller voll und der Klappenzustand wird ebenfalls zügiger verändert.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;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;bool&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;mapHatchState&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint16_t&lt;/span&gt; voltage){&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;#define maxint 32767 - 1024&#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;#define minint -32768 + 1024&#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;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int16_t&lt;/span&gt; vAggregate{&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;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; hatchOpen{false};&#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;// Divider VCC 33k T 22k GND, 12V fan will yield 4.8V at divider --&amp;gt; 983 digit&#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;// So a value of 410 is roughly 5V which allows Fan start + hatch open&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vAggregate &lt;span style="color:#f92672"&gt;=&lt;/span&gt; vAggregate &lt;span style="color:#f92672"&gt;+&lt;/span&gt; voltage &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;410&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; (vAggregate &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; maxint) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vAggregate &lt;span style="color:#f92672"&gt;=&lt;/span&gt; maxint;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hatchOpen &lt;span style="color:#f92672"&gt;=&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; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (vAggregate &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; minint) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vAggregate &lt;span style="color:#f92672"&gt;=&lt;/span&gt; minint;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hatchOpen &lt;span style="color:#f92672"&gt;=&lt;/span&gt; false;&#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;return&lt;/span&gt; hatchOpen;&#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="software--klappe-bewegen"&gt;Software : Klappe bewegen&lt;/h3&gt;&#10;&lt;p&gt;Die Funktion &lt;code&gt;setHatch()&lt;/code&gt; prüft den gewünschten Zustand der Klappe und bewegt sie, falls sie vom aktuellen Zustand abweicht, dort hin.&#10;Dabei wird dem Servo mittels &lt;code&gt;write()&lt;/code&gt; ein Zielwinkel vorgegeben, der eingestellt und gehalten werden soll. Die Bewegung geschieht graduell und über die Iteration von &lt;code&gt;hatchState&lt;/code&gt;, da die Klappe sich mir sonst viel zu ruckartig und schnell bewegt.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/htpc/htpcfancomp.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Test der Klappensteuerung durch Verstellen der Lüfterspannung&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-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;setHatch&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; open){&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; hatchState{&lt;span style="color:#ae81ff"&gt;180&lt;/span&gt;}; &lt;span style="color:#75715e"&gt;// variable to capture current position, default closed&#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;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; wasMoving{false};&#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; isMoving{false};&#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; (open &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; (hatchState &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; hatchState&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; isMoving &lt;span style="color:#f92672"&gt;=&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;else&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;open &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; (hatchState &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;180&lt;/span&gt;)) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hatchState&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; isMoving &lt;span style="color:#f92672"&gt;=&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;&#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;wasMoving &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; isMoving) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; servoHatch.&lt;span style="color:#a6e22e"&gt;attach&lt;/span&gt;(portServo); &lt;span style="color:#75715e"&gt;// connect servo on start&#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;else&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (wasMoving &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style="color:#f92672"&gt;!&lt;/span&gt;isMoving) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; servoHatch.&lt;span style="color:#a6e22e"&gt;detach&lt;/span&gt;(); &lt;span style="color:#75715e"&gt;// disconnect servo on finish&#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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; servoHatch.&lt;span style="color:#a6e22e"&gt;write&lt;/span&gt;(hatchState);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wasMoving &lt;span style="color:#f92672"&gt;=&lt;/span&gt; isMoving;&#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 Funktionen &lt;code&gt;attach()&lt;/code&gt; und &lt;code&gt;detach()&lt;/code&gt; definieren den Signalausgang zum Servo, ordnen also den Pin des Microcontrollers intern dem Servo so zu, wie er tatsächlich verdrahtet ist. Bei meinen Experimenten fand ich heraus, dass das Servo im Stillstand bei &lt;code&gt;detached&lt;/code&gt; nur wenig Energie benötigt - viel weniger, als wenn ich es &lt;code&gt;attached&lt;/code&gt; lasse und nur kein weiteres Bewegungssignal sende. Daher melde ich es für jede Bewegung an und danach wieder ab.&lt;/p&gt;&#10;&lt;h2 id="ergebnis"&gt;Ergebnis&lt;/h2&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/htpc/htpc-upgrade-complete.avif" alt="Image: HTPC insides with all upgrades applied" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Ich erhalte einen schön aufgeräumten, modernen PC. Mittels diverser Bloatware-Lösungen (die ich nach getaner Arbeit direkt wieder deinstalliere) schaffe ich es nach einigem Fluchen, alle LED-Farben aufeinander abzustimmen. Besonderes Highlight ist die automatische Klappe, welche bei höherer CPU-Last samt Lüfter aktiviert wird.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/htpc/htpc-case-complete.avif" alt="Image: HTPC case with all upgrades applied" /&gt;&lt;/figure&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;QR-codengrave GUI test&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;QR-codengrave Vektorerzeugung funktioniert&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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 00:00:00 +0000</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 class="hugo-gallery__thumb" href="#gallery-full-0" 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 src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_front.jpg" 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." class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-1" 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 src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_inside.jpg" 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." class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-2" 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 src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_top.jpg" 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." class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-3" 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 src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_inside.jpg" 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." class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;Die alternative Tonuino-Firmware im Einsatz.&lt;/div&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&#10; &lt;div class="hugo-gallery__full" aria-live="polite"&gt;&lt;figure id="gallery-full-0" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_front.jpg" 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." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;NFC-Tags sind Vögel, die zum Abspielen ins Häuschen gesetzt werden&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure id="gallery-full-1" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_inside.jpg" 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." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Anwendungsbeispiel mit Microcontroller unter Verwendung meiner ClickEncoder-Bibliothek&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure id="gallery-full-2" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_top.jpg" 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." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Anwendungsbeispiel mit Arduino in einem Gitarrenverstärker&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure id="gallery-full-3" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_inside.jpg" 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." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__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;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&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;&lt;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://raw.githubusercontent.com/Schallbert/Tonuino_alternativeFirmware/master/docs/CircuitDiagram.jpg" alt="Stromlaufplan" /&gt;&lt;/figure&gt;&#10;&lt;/div&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 00:00:00 +0000</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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;/div&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;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&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;&#10;&lt;/div&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 aufwändige 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;h2 id="update-server-architektur-im-januar-2024"&gt;Update: Server-Architektur im Januar 2024&lt;/h2&gt;&#10;&lt;p&gt;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.&lt;/p&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>