<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Renovate – teqqy</title><link>https://www.teqqy.de/tags/renovate/</link><description>Ein Blog über Technik und mehr</description><language>de</language><managingEditor>teqqy</managingEditor><lastBuildDate>Sun, 26 Jul 2026 07:23:05 +0000</lastBuildDate><generator>Hugo 0.164.0</generator><atom:link href="https://www.teqqy.de/tags/renovate/index.xml" rel="self" type="application/rss+xml"/><item><title>GitOps ohne Komodo: Webhook-basiertes Auto-Deployment für Docker Compose</title><link>https://www.teqqy.de/gitops-ohne-komodo/</link><pubDate>Wed, 03 Jun 2026 00:00:00 +0200</pubDate><lastBuildDate>Wed, 03 Jun 2026 00:00:00 +0200</lastBuildDate><guid isPermaLink="true">https://www.teqqy.de/gitops-ohne-komodo/</guid><description>Wer meinen letzten Beitrag zum Homelab Setup 2025 gelesen hat, weiß dass ich dort Komodo (externer Link) als GitOps-Werkzeug für meine Docker Compose Stacks einsetze. Das Prinzip dahinter ist gut: Git als Single Source of Truth, Renovate für automatische Version-Updates, und bei einem neuen Commit deployed Komodo die Änderungen auf das Zielsystem. Funktioniert im Grunde auch – aber Komodo hat mich mit einer Sache regelmäßig zur Weißglut gebracht: File Permissions. Das funktioniert einfach nicht konsistent. Entweder stimmen die Berechtigungen auf dem Host nicht, oder Komodo meckert beim Deployment, oder irgendwas dazwischen. Nach dem x-ten Mal hatte ich genug und wollte eine schlankere Lösung.</description><content:encoded>&lt;![CDATA[<p>Wer meinen<a href="/selfhosted-setup-2025-mein-neuer-workflow-mit-proxmox-komodo-und-gitops/">letzten Beitrag zum Homelab Setup 2025</a>
gelesen hat, weiß dass ich dort<a href="https://komo.do/" target="_blank" rel="noopener noreferrer">Komodo<svg class="link-external-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg><span class="visually-hidden"> (externer Link)</span></a>
als GitOps-Werkzeug für meine Docker Compose Stacks einsetze. Das Prinzip dahinter ist gut: Git als Single Source of Truth, Renovate für automatische Version-Updates, und bei einem neuen Commit deployed Komodo die Änderungen auf das Zielsystem. Funktioniert im Grunde auch – aber Komodo hat mich mit einer Sache regelmäßig zur Weißglut gebracht: File Permissions. Das funktioniert einfach nicht konsistent. Entweder stimmen die Berechtigungen auf dem Host nicht, oder Komodo meckert beim Deployment, oder irgendwas dazwischen. Nach dem x-ten Mal hatte ich genug und wollte eine schlankere Lösung.</p><p>Die Anforderung ist dabei überschaubar: Wenn Renovate einen neuen Commit in das Repo pusht (nach meinem Merge des Pull Requests), soll die compose-Datei auf dem Server aktualisiert und die betroffenen Container neu gestartet werden. Kein Kubernetes, kein weiteres Framework. Einfach git pull und docker compose up.</p><h2 id="das-grundprinzip">Das Grundprinzip<a href="#das-grundprinzip" class="heading-anchor" aria-label="Diesen Abschnitt verlinken"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a></h2><p>Das Setup basiert auf drei Komponenten die zusammenspielen:</p><ol><li><strong><a href="https://about.gitea.com/" target="_blank" rel="noopener noreferrer">Gitea<svg class="link-external-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg><span class="visually-hidden"> (externer Link)</span></a></strong> – mein selbst gehosteter Git-Server, auf dem die Compose-Dateien liegen</li><li><strong><a href="https://docs.renovatebot.com/" target="_blank" rel="noopener noreferrer">Renovate<svg class="link-external-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg><span class="visually-hidden"> (externer Link)</span></a></strong> – überprüft alle vier Stunden per Cronjob die verwendeten Image-Tags gegen die jeweiligen Container-Registries und öffnet bei neuen Versionen automatisch Pull Requests. Ich merge den PR, der Commit landet auf<code>main</code>.</li><li><strong><a href="https://github.com/adnanh/webhook" target="_blank" rel="noopener noreferrer">webhook<svg class="link-external-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg><span class="visually-hidden"> (externer Link)</span></a></strong> – ein einzelnes Go-Binary, das auf dem Zielserver einen HTTP-Endpunkt bereitstellt. Gitea feuert bei jedem Push einen Webhook dorthin, woraufhin das Deploy-Script ausgeführt wird.</li></ol><p>Der Datenfluss sieht so aus:</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label"/><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang=""><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><pre tabindex="0"><code>Renovate erkennt neues Image-Tag
→ PR in Gitea
→ Merge
→ Gitea feuert Webhook
→ webhook-Binary ruft deploy.sh auf
→ git pull + docker compose up -d</code></pre></div><p>Kein Polling, kein Daemon der irgendwelche Container-Registries beobachtet, kein Framework das eigene Meinungen zu File Permissions hat.</p><h2 id="warum-nicht-einfach-watchtower-oder-ein-cron-script">Warum nicht einfach Watchtower oder ein Cron-Script?<a href="#warum-nicht-einfach-watchtower-oder-ein-cron-script" class="heading-anchor" aria-label="Diesen Abschnitt verlinken"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a></h2><p>Bevor ich zu webhook gegriffen habe, hatte ich kurz zwei andere Ansätze im Kopf.</p><p><strong><a href="https://containrrr.dev/watchtower/" target="_blank" rel="noopener noreferrer">Watchtower<svg class="link-external-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg><span class="visually-hidden"> (externer Link)</span></a></strong> überwacht laufende Container und zieht neue Images automatisch. Das Problem: Watchtower reagiert auf neue Images in der Registry, nicht auf Git-Commits. Wenn Renovate einen PR öffnet und ich den merge, hat Watchtower davon keine Ahnung – der zieht einfach irgendwann wenn er selbst nachschaut. Das passt nicht zu einem Setup bei dem Git die einzige Quelle der Wahrheit sein soll. Außerdem will ich den Merge-Zeitpunkt kontrollieren, nicht Watchtower.</p><p><strong>Cron-Script mit<code>git fetch</code></strong> wäre die andere Möglichkeit – alle paar Minuten nachschauen ob es Änderungen gibt und dann deployen. Funktioniert, hat aber einen konzeptionellen Nachteil: Es ist Polling. Ich baue mit Gitea bereits eine Infrastruktur auf die Push-Events liefern kann, die sollte ich auch nutzen. Ein Webhook reagiert innerhalb von Sekunden, ein Cron-Job mit 5-Minuten-Intervall verzögert unnötig.</p><p>webhook ist der sauberste Weg: Event-getrieben, kein Extra-Daemon, ein einzelnes Binary ohne Abhängigkeiten.</p><h2 id="repo-struktur">Repo-Struktur<a href="#repo-struktur" class="heading-anchor" aria-label="Diesen Abschnitt verlinken"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a></h2><p>Ich habe pro Server ein eigenes Repository:</p><ul><li><code>apps-stack</code> für den<code>docker01</code> – dort laufen Tools wie<a href="https://docs.paperless-ngx.com/" target="_blank" rel="noopener noreferrer">Paperless-ngx<svg class="link-external-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg><span class="visually-hidden"> (externer Link)</span></a>
,<a href="https://immich.app/" target="_blank" rel="noopener noreferrer">Immich<svg class="link-external-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg><span class="visually-hidden"> (externer Link)</span></a>
,<a href="https://github.com/dani-garcia/vaultwarden" target="_blank" rel="noopener noreferrer">Vaultwarden<svg class="link-external-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg><span class="visually-hidden"> (externer Link)</span></a>
und ähnliches</li><li><code>media-stack</code> für den<code>media01</code> – dort leben alle<code>*arr</code>-Container und was sonst noch zum Medien-Stack gehört</li></ul><p>Jedes Repo enthält einfach die<code>compose.yaml</code> sowie ggf. weitere Konfigurationsdateien die einzelne Anwendungen benötigen. Nichts Besonderes.</p><h3 id="renovate-in-den-repos">Renovate in den Repos<a href="#renovate-in-den-repos" class="heading-anchor" aria-label="Diesen Abschnitt verlinken"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a></h3><p>Renovate läuft selfhosted und überprüft per Cronjob alle vier Stunden die Image-Tags in den Compose-Dateien. Dafür reicht eine minimale<code>renovate.json</code> im Repo-Root:</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label">json</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="json"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{</span></span><span style="display:flex;"><span><span style="color:#7ee787">"$schema"</span>:<span style="color:#a5d6ff">"https://docs.renovatebot.com/renovate-schema.json"</span>,</span></span><span style="display:flex;"><span><span style="color:#7ee787">"extends"</span>: [<span style="color:#a5d6ff">"config:base"</span>],</span></span><span style="display:flex;"><span><span style="color:#7ee787">"docker-compose"</span>: {</span></span><span style="display:flex;"><span><span style="color:#7ee787">"enabled"</span>:<span style="color:#79c0ff">true</span></span></span><span style="display:flex;"><span> }</span></span><span style="display:flex;"><span>}</span></span></code></pre></div></div><p>Renovate erkennt dann automatisch die Image-Tags in der Compose-Datei und öffnet PRs wenn eine neuere Version verfügbar ist. Bei Software die semantische Versionierung verwendet (also<code>major.minor.patch</code>) lässt sich das besonders fein steuern – z. B. nur Patch-Updates automatisch mergen, bei Minor-Updates erst schauen was sich geändert hat.</p><p>Für Postgres-Container würde ich Renovate übrigens ausschließen. Major-Upgrades zwischen Postgres-Versionen erfordern einen manuellen Datenbankdump und -import, das soll mir kein Bot automatisch reinschießen.</p><h2 id="webhook-installieren-und-konfigurieren">webhook installieren und konfigurieren<a href="#webhook-installieren-und-konfigurieren" class="heading-anchor" aria-label="Diesen Abschnitt verlinken"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a></h2><p><code>webhook</code> gibt es als einzelnes Binary, alternativ auch als Paket:</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label">bash</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="bash"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>apt install webhook</span></span></code></pre></div></div><p>Die Konfiguration liegt unter<code>/etc/webhook/webhook.conf</code>:</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label">yaml</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="yaml"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>-<span style="color:#7ee787">id</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">deploy</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">execute-command</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">/opt/scripts/deploy.sh</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">command-working-directory</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">/opt/media-stack</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">trigger-rule</span>:<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">match</span>:<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">type</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">payload-hmac-sha256</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">secret</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"{{getenv \"WEBHOOK_SECRET\"}}"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">parameter</span>:<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">source</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">header</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">X-Gitea-Signature</span></span></span></code></pre></div></div><p>Ein paar Anmerkungen dazu:</p><p><strong>Header-Name:</strong> Gitea schickt die Signatur als<code>X-Gitea-Signature</code> – ohne<code>-256</code>-Suffix und ohne<code>sha256=</code>-Präfix im Wert. Das klingt trivial, hat mich aber beim ersten Versuch einen Debugging-Durchgang gekostet.</p><p><strong>Secret als Umgebungsvariable:</strong> Das HMAC-Secret gehört nicht im Klartext in die Konfigurationsdatei, erst recht nicht wenn die Datei im Repo versioniert ist. Deshalb wird es per<code>getenv</code> aus der Umgebung gelesen.</p><p>Das Secret generieren:</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label">bash</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="bash"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>openssl rand -hex<span style="color:#a5d6ff">32</span></span></span></code></pre></div></div><p>Und in eine Secrets-Datei schreiben die nur root lesen kann:</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label">bash</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="bash"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># /etc/webhook/secrets.env</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff">WEBHOOK_SECRET</span><span style="color:#ff7b72;font-weight:bold">=</span>dein-generierter-string</span></span></code></pre></div></div><div class="code-block"><div class="code-block-header"><span class="code-lang-label">bash</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="bash"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>chmod<span style="color:#a5d6ff">600</span> /etc/webhook/secrets.env</span></span></code></pre></div></div><h3 id="systemd-unit">Systemd-Unit<a href="#systemd-unit" class="heading-anchor" aria-label="Diesen Abschnitt verlinken"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a></h3><div class="code-block"><div class="code-block-header"><span class="code-lang-label">ini</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="ini"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># /etc/systemd/system/webhook.service</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">[Unit]</span></span></span><span style="display:flex;"><span>Description<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">Webhook Receiver</span></span></span><span style="display:flex;"><span>After<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">network.target</span></span></span><span style="display:flex;"><span/></span><span style="display:flex;"><span><span style="color:#ff7b72">[Service]</span></span></span><span style="display:flex;"><span>User<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">teqqy # Hier den eigenen Usernamen eintragen</span></span></span><span style="display:flex;"><span>EnvironmentFile<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">/etc/webhook/secrets.env</span></span></span><span style="display:flex;"><span>ExecStart<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">/usr/bin/webhook -hooks /etc/webhook/webhook.conf -port 9000</span></span></span><span style="display:flex;"><span>Restart<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">on-failure</span></span></span><span style="display:flex;"><span/></span><span style="display:flex;"><span><span style="color:#ff7b72">[Install]</span></span></span><span style="display:flex;"><span>WantedBy<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">multi-user.target</span></span></span></code></pre></div></div><p><code>User=teqqy</code> sorgt dafür dass der webhook-Daemon und damit auch das Deploy-Script nicht als root laufen. Das ist mir wichtig – Prozesse die nur git pull und docker compose up ausführen müssen haben nichts als root verloren. Den Usernamen natürlich durch den eigenen ersetzen; der User braucht Zugriff auf das Repo-Verzeichnis und muss Mitglied der<code>docker</code>-Gruppe sein.</p><p><code>EnvironmentFile</code> lädt die<code>secrets.env</code> und stellt<code>WEBHOOK_SECRET</code> als Umgebungsvariable bereit, bevor der Prozess startet – so kommt das Secret sauber in den<code>getenv</code>-Aufruf der Konfiguration.<code>Restart=on-failure</code> sorgt dafür dass der Daemon bei einem unerwarteten Absturz automatisch neu startet, ohne dass man manuell eingreifen muss.</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label">bash</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="bash"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl enable --now webhook</span></span></code></pre></div></div><h2 id="gitea-webhook-einrichten">Gitea Webhook einrichten<a href="#gitea-webhook-einrichten" class="heading-anchor" aria-label="Diesen Abschnitt verlinken"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a></h2><p>In Gitea unter<code>Repository → Einstellungen → Webhooks → Webhook hinzufügen → Gitea</code>:</p><ul><li><strong>Ziel-URL:</strong><code>http://media01.example.com:9000/hooks/deploy</code></li><li><strong>Secret:</strong> denselben String den du oben generiert hast</li><li><strong>Trigger:</strong> Nur<code>Push</code>-Events reichen aus</li></ul><p>Gitea berechnet bei jedem Push automatisch eine HMAC-SHA256-Signatur über den Request-Body und schickt sie als<code>X-Gitea-Signature</code>-Header mit. Das webhook-Binary verifiziert die Signatur bevor es das Script ausführt – ein simpler Authorization-Header wäre hier deutlich unsicherer, weil man den Payload damit nicht gegen Manipulation absichert.</p><p>Port 9000 sollte nicht direkt ins Internet exponiert sein. In meinem Setup sind Gitea und der webhook-Daemon im selben internen Netz, der Port ist von außen nicht erreichbar. Wer den Endpunkt für eine externe Gitea-Instanz oder GitHub erreichbar machen muss, sollte das zumindest hinter einem Reverse Proxy mit IP-Beschränkung tun.</p><h2 id="das-deploy-script">Das Deploy-Script<a href="#das-deploy-script" class="heading-anchor" aria-label="Diesen Abschnitt verlinken"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a></h2><div class="code-block"><div class="code-block-header"><span class="code-lang-label">bash</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="bash"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-weight:bold;font-style:italic">#!/bin/bash</span></span></span><span style="display:flex;"><span>set -euo pipefail</span></span><span style="display:flex;"><span/></span><span style="display:flex;"><span><span style="color:#79c0ff">REPO</span><span style="color:#ff7b72;font-weight:bold">=</span>/opt/media-stack</span></span><span style="display:flex;"><span><span style="color:#79c0ff">LOG</span><span style="color:#ff7b72;font-weight:bold">=</span>/opt/deploy-logs/deploy-<span style="color:#ff7b72">$(</span>date +%Y%m%d-%H%M%S<span style="color:#ff7b72">)</span>.log</span></span><span style="display:flex;"><span/></span><span style="display:flex;"><span>exec &gt;&gt;<span style="color:#a5d6ff">"</span><span style="color:#79c0ff">$LOG</span><span style="color:#a5d6ff">"</span> 2&gt;&amp;<span style="color:#a5d6ff">1</span></span></span><span style="display:flex;"><span/></span><span style="display:flex;"><span>echo<span style="color:#a5d6ff">"=== Deploy</span><span style="color:#ff7b72">$(</span>date<span style="color:#ff7b72">)</span><span style="color:#a5d6ff"> ==="</span></span></span><span style="display:flex;"><span/></span><span style="display:flex;"><span>cd<span style="color:#a5d6ff">"</span><span style="color:#79c0ff">$REPO</span><span style="color:#a5d6ff">"</span></span></span><span style="display:flex;"><span>git pull origin main</span></span><span style="display:flex;"><span>docker compose up -d --remove-orphans</span></span><span style="display:flex;"><span>docker compose ps</span></span><span style="display:flex;"><span/></span><span style="display:flex;"><span>echo<span style="color:#a5d6ff">"=== Done ==="</span></span></span></code></pre></div></div><p><code>set -euo pipefail</code> sorgt dafür dass das Script bei jedem Fehler sofort abbricht und einen Exit-Code ≠ 0 zurückgibt. Den loggt webhook dann ebenfalls, sodass<code>journalctl -u webhook</code> bei Problemen der erste Anlaufpunkt ist.</p><p>Das Log-Verzeichnis muss dem ausführenden User gehören:</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label">bash</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="bash"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir -p /opt/deploy-logs</span></span><span style="display:flex;"><span>chown teqqy:teqqy /opt/deploy-logs</span></span></code></pre></div></div><h2 id="stolpersteine-die-ich-unterwegs-getroffen-habe">Stolpersteine die ich unterwegs getroffen habe<a href="#stolpersteine-die-ich-unterwegs-getroffen-habe" class="heading-anchor" aria-label="Diesen Abschnitt verlinken"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a></h2><p><strong><code>.git</code>-Verzeichnis Ownership:</strong> Wenn das Repo irgendwann mal als anderer User geklont oder gepullt wurde, verweigert Git den Zugriff mit einer Warnung zu &ldquo;dubiosen Besitzverhältnissen&rdquo;. Fix:</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label">bash</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="bash"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>chown -R teqqy:teqqy /opt/media-stack/.git</span></span></code></pre></div></div><p><strong><code>safe.directory</code>:</strong> In manchen Setups hilft zusätzlich:</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label">bash</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="bash"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git config --global --add safe.directory /opt/media-stack</span></span></code></pre></div></div><p><strong><code>/var/log</code> Permissions:</strong> Der ausführende User hat dort keine Schreibrechte. Deshalb das Log-Verzeichnis in einen Pfad legen der dem User gehört – ich nutze<code>/opt/deploy-logs</code>.</p><p><strong><code>*arr</code>-Container und UID/GID:</strong> Meine<code>*arr</code>-Container laufen mit<code>99:100</code> (Unraid-kompatibel wegen NFS). Beim allerersten Deployment legt Docker die Bind-Mount-Verzeichnisse mit<code>1000:1000</code> an wenn sie noch nicht existieren. Einmalig vor dem ersten Start:</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label">bash</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="bash"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>chown -R 99:100 /mnt/data/arr/</span></span></code></pre></div></div><p>Danach ist das kein Thema mehr da die Verzeichnisse bereits existieren.</p><h2 id="monitoring">Monitoring<a href="#monitoring" class="heading-anchor" aria-label="Diesen Abschnitt verlinken"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a></h2><p>Ich setze<a href="https://uptime.kuma.pet/" target="_blank" rel="noopener noreferrer">Uptime Kuma<svg class="link-external-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg><span class="visually-hidden"> (externer Link)</span></a>
ein und beobachte dort ob die Container nach einem Deployment noch sauber laufen. Das reicht für meinen Use Case: Wenn ein Container nach einem fehlgeschlagenen Update nicht mehr antwortet, schlägt Uptime Kuma an. Das deploy.log und<code>journalctl -u webhook</code> liefern dann die Details warum es schiefgelaufen ist.</p><p>Ein Dead-Man&rsquo;s-Switch (z. B. über Healthchecks.io) wäre der nächste sinnvolle Schritt wenn man auch merken will wenn ein Deploy einfach<em>ausbleibt</em> – aber für einen Media-Stack ist das für mich nicht kritisch genug um extra Aufwand zu treiben.</p><h2 id="fazit">Fazit<a href="#fazit" class="heading-anchor" aria-label="Diesen Abschnitt verlinken"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a></h2><p>Das Setup ist jetzt seit kurzem in Betrieb und macht genau was es soll – ohne dass ich irgendwas an Komodo debuggen muss. Das webhook-Binary ist stabil, der Deployment-Flow ist deterministisch, und der Gesamtoverhead beschränkt sich auf ein paar Konfigurationsdateien und ein Shell-Script.</p><p>Wer ähnliche Anforderungen hat und kein Kubernetes betreiben möchte: Das ist meiner Meinung nach der pragmatischste Weg. Keine Magie, keine Frameworks, kein Overengineering. Nur git pull und docker compose up.</p><p>Bei Fragen oder Anmerkungen erreichst du mich über die Links in der Menüleiste.</p>
]]></content:encoded><category>selfhosted</category><category>gitops</category><category>docker</category><category>gitea</category><category>renovate</category><category>homelab</category></item><item><title>Bunte Labels in GitHub (und GitLab, Gitea): Anleitung &amp; Workflow</title><link>https://www.teqqy.de/bunte-labels-beit-github-gitlab-gitea-und-co/</link><pubDate>Mon, 28 Oct 2024 18:45:00 +0100</pubDate><lastBuildDate>Mon, 28 Oct 2024 18:45:00 +0100</lastBuildDate><guid isPermaLink="true">https://www.teqqy.de/bunte-labels-beit-github-gitlab-gitea-und-co/</guid><description>Wie Menschen die mir auf social media folgen wissen, betreibe ich einen kleinen Kubernetes Cluster im GitOps Prinzip. Meine vollständige Konfiguration (externer Link) befindet sich auf der Cloud Plattform GitHub und kann dort von allen eingesehen werden. Dazu setze ich auf eine saubere Versionverwaltung beim Update der Container. Das hilft um schnell auf eine alte Version zurück zu springen. Da ich mich nicht selbst um das heraussuchen der neusten Versionen kümmern möchte, nutze ich Renovate (externer Link) als App bei GitHub. Diese schaut in die verschiedenen Dateien und vergleicht die konfigurierte Version eines Containers mit der zuletzt veröffentlichten und erstellt daraus ein pull request.</description><content:encoded>&lt;![CDATA[<p>Wie Menschen die mir auf social media folgen wissen, betreibe ich einen kleinen<a href="tags/kubernetes/">Kubernetes</a>
Cluster im GitOps Prinzip. Meine<a href="https://github.com/cbirkenbeul/homelab" target="_blank" rel="noopener noreferrer">vollständige Konfiguration<svg class="link-external-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg><span class="visually-hidden"> (externer Link)</span></a>
befindet sich auf der Cloud Plattform GitHub und kann dort von allen eingesehen werden.
Dazu setze ich auf eine saubere Versionverwaltung beim Update der Container. Das hilft um schnell auf eine alte Version zurück zu springen. Da ich mich nicht selbst um das heraussuchen der neusten Versionen kümmern möchte, nutze ich<a href="https://github.com/renovatebot/renovate" target="_blank" rel="noopener noreferrer">Renovate<svg class="link-external-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg><span class="visually-hidden"> (externer Link)</span></a>
als App bei GitHub. Diese schaut in die verschiedenen Dateien und vergleicht die konfigurierte Version eines Containers mit der zuletzt veröffentlichten und erstellt daraus ein pull request.</p><p>Im Standard sind alle Labels bei den Plattformen wie GitHub, Gitlab oder<a href="https://www.youtube.com/watch?v=2h-gkICAcbk" target="_blank" rel="noopener noreferrer">gitea<svg class="link-external-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg><span class="visually-hidden"> (externer Link)</span></a>
in grau gehalten. Hier fehlt also etwas die Übersicht zur schnelleren Einschätzung der Situation. Aus diesem Grund verwende ich ein kleinen Workflow der mir die von Renovate angehäfteten Labels bunt einfärbt. Ich nutze das Vorgehen auf GitHub, sollte aber fast identisch auch bei GitLab oder gitea funktionieren.</p><p>Ich gehe davon aus, dass es bereit ein vorhandenes Repository gibt und sich dies lokal auf deiner Workstation befindet. Entsprechend erstellen wir im Hauptverzeichnis des Repos einen Unterordner mit dem Namen<code>.github</code>. Auf anderen Plattformen sollte er analog zur Plattform heißen.</p><p>In diesem Unterordner erstellen wir einen weiteren Order mit dem Namen<code>workflows</code>. Sämtliche<code>y(a)ml</code>Dateien werden hier als eingenständiger Workflow auf der jeweiligen Plattform aufgeführt.</p><p>Nun können wir auch den Workflow bzw. GitHub Action anlegen. Ich habe sie bei mir<code>label-sync.yaml</code> genannt und folgendes in die Datei kopiert:</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label">yaml</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="yaml"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff7b72">---</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"Label Sync"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#7ee787">on</span>:<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">workflow_dispatch</span>:<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">push</span>:<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">branches</span>:<span style="color:#6e7681"/>[<span style="color:#a5d6ff">"main"</span>]<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">paths</span>:<span style="color:#6e7681"/>[<span style="color:#a5d6ff">".github/labels.yaml"</span>]<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#7ee787">jobs</span>:<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">label-sync</span>:<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">Label Sync</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">runs-on</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">ubuntu-latest</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">steps</span>:<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">Checkout</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">uses</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">actions/checkout@v4</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">with</span>:<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">sparse-checkout</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">.github/labels.yaml</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">Sync Labels</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">uses</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">EndBug/label-sync@v2</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">with</span>:<span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">config-file</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">.github/labels.yaml</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">delete-other-labels</span>:<span style="color:#6e7681"/><span style="color:#79c0ff">true</span></span></span></code></pre></div></div><p>Glücklicherweise brauchen wir hierfür ein Bot oder ähnliches, sondern es funktioniert direkt. Wie du gegebenenfalls in dem workflow siehst, verweisen wir auf eine Datei mit dem Namen<code>labels.yaml</code>. Diese müssen wir noch in dem Ordner<code>.github</code> anlegen und befüllen.</p><p>In diese Datei kommen dann sämtliche Labels mit dem entsprechenden HTML-Farbcode. Bei mir sieht die Datei so aus:</p><div class="code-block"><div class="code-block-header"><span class="code-lang-label">yaml</span><button class="code-copy-btn" type="button" aria-label="Code kopieren" data-umami-event="Code Copy" data-umami-event-lang="yaml"><svg class="icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><svg class="icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg></button></div><div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff7b72">---</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Areas</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">area/ansible</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"0e8a16"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">area/docs</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"0e8a16"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">area/github</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"0e8a16"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">area/kubernetes</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"0e8a16"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">area/terraform</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"0e8a16"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Clusters</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">cluster/main</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"ffc300"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">cluster/storage</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"ffc300"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Renovate Types</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">renovate/ansible</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"027fa0"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">renovate/container</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"027fa0"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">renovate/github-action</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"027fa0"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">renovate/grafana-dashboard</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"027fa0"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">renovate/github-release</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"027fa0"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">renovate/helm</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"027fa0"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">renovate/terraform</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"027fa0"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Semantic Types</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">type/digest</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"ffeC19"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">type/patch</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"ffeC19"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">type/minor</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"ff9800"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">type/major</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"f6412d"</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Uncategorized</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span>-<span style="color:#7ee787">name</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">hold</span><span style="color:#6e7681"/></span></span><span style="display:flex;"><span><span style="color:#6e7681"/><span style="color:#7ee787">color</span>:<span style="color:#6e7681"/><span style="color:#a5d6ff">"ee0701"</span></span></span></code></pre></div></div><p>Wir können jetzt unsere Änderungen commiten und pushen. Beim nächsten push wird dann der Workflow angestoßen und ausgeführt. Nun sollten alle Labels die ihr als Issue oder Pull Request habt in der entsprechenden Farbe dargestellt werden. Bei mir sieht das z. B. so aus:<img src="/bunte-labels-beit-github-gitlab-gitea-und-co/images/bunte-labels-bei-github_hu_f7df1012ba3dde9.webp" srcset="/bunte-labels-beit-github-gitlab-gitea-und-co/images/bunte-labels-bei-github_hu_865c588474f730e9.webp 384w, /bunte-labels-beit-github-gitlab-gitea-und-co/images/bunte-labels-bei-github_hu_f7df1012ba3dde9.webp 768w" sizes="(max-width: 768px) 100vw, 768px" alt="GitHub Screenshot für bunte Labels" loading="eager" fetchpriority="high" decoding="async" width="768" height="206"/>
]]></content:encoded><category>github</category><category>gitlab</category><category>gitea</category><category>renovate</category></item></channel></rss>